mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01393464ea | ||
|
|
97a152b841 | ||
|
|
537ccd2fbe | ||
|
|
86e421fba9 | ||
|
|
c5201abc4a | ||
|
|
4eda1b79fa | ||
|
|
030f7c4a7b | ||
|
|
6c60c8f3f3 | ||
|
|
2660754dbc | ||
|
|
94a867c845 | ||
|
|
1b5474c8e1 | ||
|
|
b135fa7a8b | ||
|
|
0ed93552f4 | ||
|
|
ffd7930312 | ||
|
|
131c8ccfcd | ||
|
|
2eb404de13 | ||
|
|
4eaf628120 | ||
|
|
3ed46e0668 | ||
|
|
b4c6956a59 | ||
|
|
b9fc13db69 | ||
|
|
1567d50a27 | ||
|
|
c110091d36 | ||
|
|
20213d01a3 | ||
|
|
7747abdc15 | ||
|
|
b75438f986 | ||
|
|
7d9642d876 | ||
|
|
b92d874c8b | ||
|
|
1f2715d1d2 | ||
|
|
9bddac0dcf | ||
|
|
97e70906fa | ||
|
|
1a3357648f | ||
|
|
9dc171e3bb | ||
|
|
eeda54fe63 | ||
|
|
933ac0e05c | ||
|
|
b8400c9faf | ||
|
|
5bf2c00a35 | ||
|
|
83d051a8d9 | ||
|
|
eaa3226ef7 | ||
|
|
36ab251c07 | ||
|
|
45c290660a | ||
|
|
0fef690bd5 | ||
|
|
6fdc4c5282 | ||
|
|
3fdc7243f3 | ||
|
|
7bf6d4c9d5 | ||
|
|
a178d15baf | ||
|
|
b312dbe920 | ||
|
|
174b8294c8 | ||
|
|
58627eff2e | ||
|
|
ed01babd07 | ||
|
|
e2ef349f56 | ||
|
|
2cd4346df6 | ||
|
|
6cc7ad15b6 | ||
|
|
6349da612b |
No files matched your search
+71
@@ -1,6 +1,77 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 9.3.2 (08/02/2024)
|
||||
### Bug Fixes
|
||||
* *Make DestroyDB supports slow deletion when it's configured in `SstFileManager`. The slow deletion is subject to the configured `rate_bytes_per_sec`, but not subject to the `max_trash_db_ratio`.
|
||||
* Fix a race condition in pessimistic transactions that could allow multiple transactions with the same name to be registered simultaneously, resulting in a crash or other unpredictable behavior.
|
||||
|
||||
## 9.3.1 (05/25/2024)
|
||||
### Bug Fixes
|
||||
* [internal only] Build script improvement
|
||||
|
||||
## 9.3.0 (05/17/2024)
|
||||
### New Features
|
||||
* Optimistic transactions and pessimistic transactions with the WriteCommitted policy now support the `GetEntity` API.
|
||||
* Added new `Iterator` property, "rocksdb.iterator.is-value-pinned", for checking whether the `Slice` returned by `Iterator::value()` can be used until the `Iterator` is destroyed.
|
||||
* Optimistic transactions and WriteCommitted pessimistic transactions now support the `MultiGetEntity` API.
|
||||
* Optimistic transactions and pessimistic transactions with the WriteCommitted policy now support the `PutEntity` API. Support for read APIs and other write policies (WritePrepared, WriteUnprepared) will be added later.
|
||||
|
||||
### Public API Changes
|
||||
* Exposed block based metadata cache options via C API
|
||||
* Exposed compaction pri via c api.
|
||||
* Add a kAdmPolicyAllowAll option to TieredAdmissionPolicy that admits all blocks evicted from the primary block cache into the compressed secondary cache.
|
||||
|
||||
### Behavior Changes
|
||||
* CompactRange() with change_level=true on a CF with FIFO compaction will return Status::NotSupported().
|
||||
* External file ingestion with FIFO compaction will always ingest to L0.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug for databases using `DBOptions::allow_2pc == true` (all `TransactionDB`s except `OptimisticTransactionDB`) that have exactly one column family. Due to a missing WAL sync, attempting to open the DB could have returned a `Status::Corruption` with a message like "SST file is ahead of WALs".
|
||||
* Fix a bug in CreateColumnFamilyWithImport() where if multiple CFs are imported, we were not resetting files' epoch number and L0 files can have overlapping key range but the same epoch number.
|
||||
* Fixed race conditions when `ColumnFamilyOptions::inplace_update_support == true` between user overwrites and reads on the same key.
|
||||
* Fix a bug where `CompactFiles()` can compact files of range conflict with other ongoing compactions' when `preclude_last_level_data_seconds > 0` is used
|
||||
* Fixed a false positive `Status::Corruption` reported when reopening a DB that used `DBOptions::recycle_log_file_num > 0` and `DBOptions::wal_compression != kNoCompression`.
|
||||
* While WAL is locked with LockWAL(), some operations like Flush() and IngestExternalFile() are now blocked as they should have been.
|
||||
* Fixed a bug causing stale memory access when using the TieredSecondaryCache with an NVM secondary cache, and a file system that supports return an FS allocated buffer for MultiRead (FSSupportedOps::kFSBuffer is set).
|
||||
|
||||
## 9.2.0 (05/01/2024)
|
||||
### New Features
|
||||
* Added two options `deadline` and `max_size_bytes` for CacheDumper to exit early
|
||||
* Added a new API `GetEntityFromBatchAndDB` to `WriteBatchWithIndex` that can be used for wide-column point lookups with read-your-own-writes consistency. Similarly to `GetFromBatchAndDB`, the API can combine data from the write batch with data from the underlying database if needed. See the API comments for more details.
|
||||
* [Experimental] Introduce two new cross-column-family iterators - CoalescingIterator and AttributeGroupIterator. The CoalescingIterator enables users to iterate over multiple column families and access their values and columns. During this iteration, if the same key exists in more than one column family, the keys in the later column family will overshadow the previous ones. The AttributeGroupIterator allows users to gather wide columns per Column Family and create attribute groups while iterating over keys across all CFs.
|
||||
* Added a new API `MultiGetEntityFromBatchAndDB` to `WriteBatchWithIndex` that can be used for batched wide-column point lookups with read-your-own-writes consistency. Similarly to `MultiGetFromBatchAndDB`, the API can combine data from the write batch with data from the underlying database if needed. See the API comments for more details.
|
||||
* *Adds a `SstFileReader::NewTableIterator` API to support programmatically read a SST file as a raw table file.
|
||||
* Add an option to `WaitForCompactOptions` - `wait_for_purge` to make `WaitForCompact()` API wait for background purge to complete
|
||||
|
||||
### Public API Changes
|
||||
* DeleteRange() will return NotSupported() if row_cache is configured since they don't work together in some cases.
|
||||
* Deprecated `CompactionOptions::compression` since `CompactionOptions`'s API for configuring compression was incomplete, unsafe, and likely unnecessary
|
||||
* Using `OptionChangeMigration()` to migrate from non-FIFO to FIFO compaction
|
||||
with `Options::compaction_options_fifo.max_table_files_size` > 0 can cause
|
||||
the whole DB to be dropped right after migration if the migrated data is larger than
|
||||
`max_table_files_size`
|
||||
|
||||
### Behavior Changes
|
||||
* Enabling `BlockBasedTableOptions::block_align` is now incompatible (i.e., APIs will return `Status::InvalidArgument`) with more ways of enabling compression: `CompactionOptions::compression`, `ColumnFamilyOptions::compression_per_level`, and `ColumnFamilyOptions::bottommost_compression`.
|
||||
* Changed the default value of `CompactionOptions::compression` to `kDisableCompressionOption`, which means the compression type is determined by the `ColumnFamilyOptions`.
|
||||
* `BlockBasedTableOptions::optimize_filters_for_memory` is now set to true by default. When `partition_filters=false`, this could lead to somewhat increased average RSS memory usage by the block cache, but this "extra" usage is within the allowed memory budget and should make memory usage more consistent (by minimizing internal fragmentation for more kinds of blocks).
|
||||
* Dump all keys for cache dumper impl if `SetDumpFilter()` is not called
|
||||
* `CompactRange()` with `CompactRangeOptions::change_level = true` and `CompactRangeOptions::target_level = 0` that ends up moving more than 1 file from non-L0 to L0 will return `Status::Aborted()`.
|
||||
* On distributed file systems that support file system level checksum verification and reconstruction reads, RocksDB will now retry a file read if the initial read fails RocksDB block level or record level checksum verification. This applies to MANIFEST file reads when the DB is opened, and to SST file reads at all times.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug causing `VerifyFileChecksums()` to return false-positive corruption under `BlockBasedTableOptions::block_align=true`
|
||||
* Provide consistent view of the database across the column families for `NewIterators()` API.
|
||||
* Fixed feature interaction bug for `DeleteRange()` together with `ColumnFamilyOptions::memtable_insert_with_hint_prefix_extractor`. The impact of this bug would likely be corruption or crashing.
|
||||
* Fixed hang in `DisableManualCompactions()` where compactions waiting to be scheduled due to conflicts would not be canceled promptly
|
||||
* Fixed a regression when `ColumnFamilyOptions::max_successive_merges > 0` where the CPU overhead for deciding whether to merge could have increased unless the user had set the option `ColumnFamilyOptions::strict_max_successive_merges`
|
||||
* Fixed a bug in `MultiGet()` and `MultiGetEntity()` together with blob files (`ColumnFamilyOptions::enable_blob_files == true`). An error looking up one of the keys could cause the results to be wrong for other keys for which the statuses were `Status::OK`.
|
||||
* Fixed a bug where wrong padded bytes are used to generate file checksum and `DataVerificationInfo::checksum` upon file creation
|
||||
* Correctly implemented the move semantics of `PinnableWideColumns`.
|
||||
* Fixed a bug when the recycle_log_file_num in DBOptions is changed from 0 to non-zero when a DB is reopened. On a subsequent reopen, if a log file created when recycle_log_file_num==0 was reused previously, is alive and is empty, we could end up inserting stale WAL records into the memtable.
|
||||
* *Fix a bug where obsolete files' deletion during DB::Open are not rate limited with `SstFilemManager`'s slow deletion feature even if it's configured.
|
||||
|
||||
## 9.1.0 (03/22/2024)
|
||||
### New Features
|
||||
* Added an option, `GetMergeOperandsOptions::continue_cb`, to give users the ability to end `GetMergeOperands()`'s lookup process before all merge operands were found.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
|
||||
@@ -7,6 +7,7 @@ rocksdb_target_header_template = """# This file \100generated by:
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
"""
|
||||
|
||||
|
||||
Vendored
+6
-3
@@ -134,12 +134,14 @@ bool CacheWithSecondaryAdapter::EvictionHandler(const Slice& key,
|
||||
auto obj = target_->Value(handle);
|
||||
// Ignore dummy entry
|
||||
if (obj != kDummyObj) {
|
||||
bool hit = false;
|
||||
bool force = false;
|
||||
if (adm_policy_ == TieredAdmissionPolicy::kAdmPolicyAllowCacheHits) {
|
||||
hit = was_hit;
|
||||
force = was_hit;
|
||||
} else if (adm_policy_ == TieredAdmissionPolicy::kAdmPolicyAllowAll) {
|
||||
force = true;
|
||||
}
|
||||
// Spill into secondary cache.
|
||||
secondary_cache_->Insert(key, obj, helper, hit).PermitUncheckedError();
|
||||
secondary_cache_->Insert(key, obj, helper, force).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
// Never takes ownership of obj
|
||||
@@ -661,6 +663,7 @@ std::shared_ptr<Cache> NewTieredCache(const TieredCacheOptions& _opts) {
|
||||
break;
|
||||
case TieredAdmissionPolicy::kAdmPolicyPlaceholder:
|
||||
case TieredAdmissionPolicy::kAdmPolicyAllowCacheHits:
|
||||
case TieredAdmissionPolicy::kAdmPolicyAllowAll:
|
||||
if (opts.nvm_sec_cache) {
|
||||
valid_adm_policy = false;
|
||||
}
|
||||
|
||||
Vendored
+164
-1
@@ -816,11 +816,174 @@ TEST_P(DBTieredAdmPolicyTest, CompressedOnlyTest) {
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_P(DBTieredAdmPolicyTest, CompressedCacheAdmission) {
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
// We want a block cache of size 5KB, and a compressed secondary cache of
|
||||
// size 5KB. However, we specify a block cache size of 256KB here in order
|
||||
// to take into account the cache reservation in the block cache on
|
||||
// behalf of the compressed cache. The unit of cache reservation is 256KB.
|
||||
// The effective block cache capacity will be calculated as 256 + 5 = 261KB,
|
||||
// and 256KB will be reserved for the compressed cache, leaving 10KB for
|
||||
// the primary block cache. We only have to worry about this here because
|
||||
// the cache size is so small.
|
||||
table_options.block_cache = NewCache(256 * 1024, 5 * 1024, 0, GetParam());
|
||||
table_options.block_size = 4 * 1024;
|
||||
table_options.cache_index_and_filter_blocks = false;
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
size_t comp_cache_usage = compressed_secondary_cache()->TEST_GetUsage();
|
||||
// Disable paranoid_file_checks so that flush will not read back the newly
|
||||
// written file
|
||||
options.paranoid_file_checks = false;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
const int N = 256;
|
||||
for (int i = 0; i < N; i++) {
|
||||
std::string p_v;
|
||||
test::CompressibleString(&rnd, 0.5, 1007, &p_v);
|
||||
ASSERT_OK(Put(Key(i), p_v));
|
||||
}
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// The second Get (for 5) will evict the data block loaded by the first
|
||||
// Get, which will be admitted into the compressed secondary cache only
|
||||
// for the kAdmPolicyAllowAll policy
|
||||
std::string v = Get(Key(0));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
|
||||
v = Get(Key(5));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
|
||||
if (GetParam() == TieredAdmissionPolicy::kAdmPolicyAllowAll) {
|
||||
ASSERT_GT(compressed_secondary_cache()->TEST_GetUsage(),
|
||||
comp_cache_usage + 128);
|
||||
} else {
|
||||
ASSERT_LT(compressed_secondary_cache()->TEST_GetUsage(),
|
||||
comp_cache_usage + 128);
|
||||
}
|
||||
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_F(DBTieredSecondaryCacheTest, FSBufferTest) {
|
||||
class WrapFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit WrapFS(const std::shared_ptr<FileSystem>& _target)
|
||||
: FileSystemWrapper(_target) {}
|
||||
~WrapFS() override {}
|
||||
const char* Name() const override { return "WrapFS"; }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
class WrappedRandomAccessFile : public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
explicit WrappedRandomAccessFile(
|
||||
std::unique_ptr<FSRandomAccessFile>& file)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(file)) {}
|
||||
|
||||
IOStatus MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
for (size_t i = 0; i < num_reqs; ++i) {
|
||||
FSReadRequest& req = reqs[i];
|
||||
FSAllocationPtr buffer(new char[req.len], [](void* ptr) {
|
||||
delete[] static_cast<char*>(ptr);
|
||||
});
|
||||
req.fs_scratch = std::move(buffer);
|
||||
req.status = Read(req.offset, req.len, options, &req.result,
|
||||
static_cast<char*>(req.fs_scratch.get()), dbg);
|
||||
}
|
||||
return IOStatus::OK();
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, opts, &file, dbg);
|
||||
EXPECT_OK(s);
|
||||
result->reset(new WrappedRandomAccessFile(file));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
void SupportedOps(int64_t& supported_ops) override {
|
||||
supported_ops = 1 << FSSupportedOps::kAsyncIO;
|
||||
supported_ops |= 1 << FSSupportedOps::kFSBuffer;
|
||||
}
|
||||
};
|
||||
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<WrapFS> wrap_fs =
|
||||
std::make_shared<WrapFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> wrap_env(new CompositeEnvWrapper(env_, wrap_fs));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = NewCache(250 * 1024, 20 * 1024, 256 * 1024,
|
||||
TieredAdmissionPolicy::kAdmPolicyAuto,
|
||||
/*ready_before_wait=*/true);
|
||||
table_options.block_size = 4 * 1024;
|
||||
table_options.cache_index_and_filter_blocks = false;
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.env = wrap_env.get();
|
||||
|
||||
options.paranoid_file_checks = false;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
const int N = 256;
|
||||
for (int i = 0; i < N; i++) {
|
||||
std::string p_v;
|
||||
test::CompressibleString(&rnd, 0.5, 1007, &p_v);
|
||||
ASSERT_OK(Put(Key(i), p_v));
|
||||
}
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
|
||||
keys.push_back(Key(0));
|
||||
keys.push_back(Key(4));
|
||||
keys.push_back(Key(8));
|
||||
values = MultiGet(keys, /*snapshot=*/nullptr, /*async=*/true);
|
||||
ASSERT_EQ(values.size(), keys.size());
|
||||
for (const auto& value : values) {
|
||||
ASSERT_EQ(1007, value.size());
|
||||
}
|
||||
ASSERT_EQ(nvm_sec_cache()->num_insert_saved(), 3u);
|
||||
ASSERT_EQ(nvm_sec_cache()->num_misses(), 3u);
|
||||
ASSERT_EQ(nvm_sec_cache()->num_hits(), 0u);
|
||||
|
||||
std::string v = Get(Key(12));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
ASSERT_EQ(nvm_sec_cache()->num_insert_saved(), 4u);
|
||||
ASSERT_EQ(nvm_sec_cache()->num_misses(), 4u);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(BLOCK_CACHE_MISS), 4u);
|
||||
|
||||
Close();
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
DBTieredAdmPolicyTest, DBTieredAdmPolicyTest,
|
||||
::testing::Values(TieredAdmissionPolicy::kAdmPolicyAuto,
|
||||
TieredAdmissionPolicy::kAdmPolicyPlaceholder,
|
||||
TieredAdmissionPolicy::kAdmPolicyAllowCacheHits));
|
||||
TieredAdmissionPolicy::kAdmPolicyAllowCacheHits,
|
||||
TieredAdmissionPolicy::kAdmPolicyAllowAll));
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
@@ -2745,6 +2745,24 @@ void rocksdb_options_set_block_based_table_factory(
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_top_level_index_pinning_tier(
|
||||
rocksdb_block_based_table_options_t* options, int v) {
|
||||
options->rep.metadata_cache_options.top_level_index_pinning =
|
||||
static_cast<ROCKSDB_NAMESPACE::PinningTier>(v);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_partition_pinning_tier(
|
||||
rocksdb_block_based_table_options_t* options, int v) {
|
||||
options->rep.metadata_cache_options.partition_pinning =
|
||||
static_cast<ROCKSDB_NAMESPACE::PinningTier>(v);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_unpartitioned_pinning_tier(
|
||||
rocksdb_block_based_table_options_t* options, int v) {
|
||||
options->rep.metadata_cache_options.unpartitioned_pinning =
|
||||
static_cast<ROCKSDB_NAMESPACE::PinningTier>(v);
|
||||
}
|
||||
|
||||
rocksdb_cuckoo_table_options_t* rocksdb_cuckoo_options_create() {
|
||||
return new rocksdb_cuckoo_table_options_t;
|
||||
}
|
||||
@@ -3930,6 +3948,14 @@ void rocksdb_options_set_fifo_compaction_options(
|
||||
opt->rep.compaction_options_fifo = fifo->rep;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_compaction_pri(rocksdb_options_t* opt, int pri) {
|
||||
opt->rep.compaction_pri = static_cast<ROCKSDB_NAMESPACE::CompactionPri>(pri);
|
||||
}
|
||||
|
||||
int rocksdb_options_get_compaction_pri(rocksdb_options_t* opt) {
|
||||
return opt->rep.compaction_pri;
|
||||
}
|
||||
|
||||
char* rocksdb_options_statistics_get_string(rocksdb_options_t* opt) {
|
||||
ROCKSDB_NAMESPACE::Statistics* statistics = opt->rep.statistics.get();
|
||||
if (statistics) {
|
||||
|
||||
+11
-1
@@ -704,6 +704,10 @@ int main(int argc, char** argv) {
|
||||
rocksdb_block_based_options_set_block_cache(table_options, cache);
|
||||
rocksdb_block_based_options_set_data_block_index_type(table_options, 1);
|
||||
rocksdb_block_based_options_set_data_block_hash_ratio(table_options, 0.75);
|
||||
rocksdb_block_based_options_set_top_level_index_pinning_tier(table_options,
|
||||
1);
|
||||
rocksdb_block_based_options_set_partition_pinning_tier(table_options, 2);
|
||||
rocksdb_block_based_options_set_unpartitioned_pinning_tier(table_options, 3);
|
||||
rocksdb_options_set_block_based_table_factory(options, table_options);
|
||||
|
||||
rocksdb_options_set_compression(options, rocksdb_no_compression);
|
||||
@@ -2041,7 +2045,6 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_advise_random_on_open(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_advise_random_on_open(o));
|
||||
|
||||
|
||||
rocksdb_options_set_use_adaptive_mutex(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_use_adaptive_mutex(o));
|
||||
|
||||
@@ -2112,6 +2115,9 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_compaction_style(o, 2);
|
||||
CheckCondition(2 == rocksdb_options_get_compaction_style(o));
|
||||
|
||||
rocksdb_options_set_compaction_pri(o, 4);
|
||||
CheckCondition(4 == rocksdb_options_get_compaction_pri(o));
|
||||
|
||||
rocksdb_options_set_atomic_flush(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_atomic_flush(o));
|
||||
|
||||
@@ -2616,6 +2622,10 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(1 == rocksdb_options_get_compaction_style(copy));
|
||||
CheckCondition(2 == rocksdb_options_get_compaction_style(o));
|
||||
|
||||
rocksdb_options_set_compaction_pri(copy, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_compaction_pri(copy));
|
||||
CheckCondition(4 == rocksdb_options_get_compaction_pri(o));
|
||||
|
||||
rocksdb_options_set_atomic_flush(copy, 0);
|
||||
CheckCondition(0 == rocksdb_options_get_atomic_flush(copy));
|
||||
CheckCondition(1 == rocksdb_options_get_atomic_flush(o));
|
||||
|
||||
@@ -1280,6 +1280,21 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
// saved_seq_for_penul_check_ is populated in `NextFromInput` when the
|
||||
// entry's sequence number is non zero and validity context for output this
|
||||
// entry is kSwapPreferredSeqno for use in `DecideOutputLevel`. It should be
|
||||
// cleared out here unconditionally. Otherwise, it may end up getting consumed
|
||||
// incorrectly by a different entry.
|
||||
SequenceNumber seq_for_range_check =
|
||||
(saved_seq_for_penul_check_.has_value() &&
|
||||
saved_seq_for_penul_check_.value() != kMaxSequenceNumber)
|
||||
? saved_seq_for_penul_check_.value()
|
||||
: ikey_.sequence;
|
||||
saved_seq_for_penul_check_ = std::nullopt;
|
||||
ParsedInternalKey ikey_for_range_check = ikey_;
|
||||
if (seq_for_range_check != ikey_.sequence) {
|
||||
ikey_for_range_check.sequence = seq_for_range_check;
|
||||
}
|
||||
if (output_to_penultimate_level_) {
|
||||
// If it's decided to output to the penultimate level, but unsafe to do so,
|
||||
// still output to the last level. For example, moving the data from a lower
|
||||
@@ -1287,16 +1302,6 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
// considered unsafe, because the key may conflict with higher-level SSTs
|
||||
// not from this compaction.
|
||||
// TODO: add statistic for declined output_to_penultimate_level
|
||||
SequenceNumber seq_for_range_check =
|
||||
(saved_seq_for_penul_check_.has_value() &&
|
||||
saved_seq_for_penul_check_.value() != kMaxSequenceNumber)
|
||||
? saved_seq_for_penul_check_.value()
|
||||
: ikey_.sequence;
|
||||
ParsedInternalKey ikey_for_range_check = ikey_;
|
||||
if (seq_for_range_check != ikey_.sequence) {
|
||||
ikey_for_range_check.sequence = seq_for_range_check;
|
||||
saved_seq_for_penul_check_ = std::nullopt;
|
||||
}
|
||||
bool safe_to_penultimate_level =
|
||||
compaction_->WithinPenultimateLevelOutputRange(ikey_for_range_check);
|
||||
if (!safe_to_penultimate_level) {
|
||||
@@ -1310,7 +1315,7 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
// snapshot is released before enabling `last_level_temperature` feature
|
||||
// We will migrate the feature to `last_level_temperature` and maybe make
|
||||
// it not dynamically changeable.
|
||||
if (ikey_.sequence > earliest_snapshot_) {
|
||||
if (seq_for_range_check > earliest_snapshot_) {
|
||||
status_ = Status::Corruption(
|
||||
"Unsafe to store Seq later than snapshot in the last level if "
|
||||
"per_key_placement is enabled");
|
||||
|
||||
@@ -340,8 +340,8 @@ Compaction* CompactionPicker::CompactFiles(
|
||||
#ifndef NDEBUG
|
||||
assert(input_files.size());
|
||||
// This compaction output should not overlap with a running compaction as
|
||||
// `SanitizeCompactionInputFiles` should've checked earlier and db mutex
|
||||
// shouldn't have been released since.
|
||||
// `SanitizeAndConvertCompactionInputFiles` should've checked earlier and db
|
||||
// mutex shouldn't have been released since.
|
||||
int start_level = Compaction::kInvalidLevel;
|
||||
for (const auto& in : input_files) {
|
||||
// input_files should already be sorted by level
|
||||
@@ -1040,19 +1040,18 @@ Status CompactionPicker::SanitizeCompactionInputFilesForAllLevels(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (RangeOverlapWithCompaction(smallestkey, largestkey, output_level)) {
|
||||
return Status::Aborted(
|
||||
"A running compaction is writing to the same output level in an "
|
||||
"overlapping key range");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status CompactionPicker::SanitizeCompactionInputFiles(
|
||||
Status CompactionPicker::SanitizeAndConvertCompactionInputFiles(
|
||||
std::unordered_set<uint64_t>* input_files,
|
||||
const ColumnFamilyMetaData& cf_meta, const int output_level) const {
|
||||
const ColumnFamilyMetaData& cf_meta, const int output_level,
|
||||
const VersionStorageInfo* vstorage,
|
||||
std::vector<CompactionInputFiles>* converted_input_files) const {
|
||||
assert(static_cast<int>(cf_meta.levels.size()) - 1 ==
|
||||
cf_meta.levels[cf_meta.levels.size() - 1].level);
|
||||
assert(converted_input_files);
|
||||
|
||||
if (output_level >= static_cast<int>(cf_meta.levels.size())) {
|
||||
return Status::InvalidArgument(
|
||||
"Output level for column family " + cf_meta.name +
|
||||
@@ -1078,7 +1077,6 @@ Status CompactionPicker::SanitizeCompactionInputFiles(
|
||||
|
||||
Status s = SanitizeCompactionInputFilesForAllLevels(input_files, cf_meta,
|
||||
output_level);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1119,6 +1117,22 @@ Status CompactionPicker::SanitizeCompactionInputFiles(
|
||||
}
|
||||
}
|
||||
|
||||
s = GetCompactionInputsFromFileNumbers(converted_input_files, input_files,
|
||||
vstorage, CompactionOptions());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(converted_input_files->size() > 0);
|
||||
if (output_level != 0 &&
|
||||
FilesRangeOverlapWithCompaction(
|
||||
*converted_input_files, output_level,
|
||||
Compaction::EvaluatePenultimateLevel(
|
||||
vstorage, ioptions_, (*converted_input_files)[0].level,
|
||||
output_level))) {
|
||||
return Status::Aborted(
|
||||
"A running compaction is writing to the same output level(s) in an "
|
||||
"overlapping key range");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
|
||||
@@ -86,16 +86,20 @@ class CompactionPicker {
|
||||
|
||||
virtual bool NeedsCompaction(const VersionStorageInfo* vstorage) const = 0;
|
||||
|
||||
// Sanitize the input set of compaction input files.
|
||||
// When the input parameters do not describe a valid compaction, the
|
||||
// function will try to fix the input_files by adding necessary
|
||||
// files. If it's not possible to conver an invalid input_files
|
||||
// into a valid one by adding more files, the function will return a
|
||||
// non-ok status with specific reason.
|
||||
//
|
||||
Status SanitizeCompactionInputFiles(std::unordered_set<uint64_t>* input_files,
|
||||
const ColumnFamilyMetaData& cf_meta,
|
||||
const int output_level) const;
|
||||
// Sanitize the input set of compaction input files and convert it to
|
||||
// `std::vector<CompactionInputFiles>` in the output parameter
|
||||
// `converted_input_files`.
|
||||
// When the input parameters do not describe a valid
|
||||
// compaction, the function will try to fix the input_files by adding
|
||||
// necessary files. If it's not possible to convert an invalid input_files
|
||||
// into a valid one by adding more files, the function will return a
|
||||
// non-ok status with specific reason.
|
||||
//
|
||||
Status SanitizeAndConvertCompactionInputFiles(
|
||||
std::unordered_set<uint64_t>* input_files,
|
||||
const ColumnFamilyMetaData& cf_meta, const int output_level,
|
||||
const VersionStorageInfo* vstorage,
|
||||
std::vector<CompactionInputFiles>* converted_input_files) const;
|
||||
|
||||
// Free up the files that participated in a compaction
|
||||
//
|
||||
@@ -109,8 +113,8 @@ class CompactionPicker {
|
||||
// object.
|
||||
//
|
||||
// Caller must provide a set of input files that has been passed through
|
||||
// `SanitizeCompactionInputFiles` earlier. The lock should not be released
|
||||
// between that call and this one.
|
||||
// `SanitizeAndConvertCompactionInputFiles` earlier. The lock should not be
|
||||
// released between that call and this one.
|
||||
Compaction* CompactFiles(const CompactionOptions& compact_options,
|
||||
const std::vector<CompactionInputFiles>& input_files,
|
||||
int output_level, VersionStorageInfo* vstorage,
|
||||
@@ -120,6 +124,7 @@ class CompactionPicker {
|
||||
|
||||
// Converts a set of compaction input file numbers into
|
||||
// a list of CompactionInputFiles.
|
||||
// TODO(hx235): remove the unused paramter `compact_options`
|
||||
Status GetCompactionInputsFromFileNumbers(
|
||||
std::vector<CompactionInputFiles>* input_files,
|
||||
std::unordered_set<uint64_t>* input_set,
|
||||
@@ -225,8 +230,8 @@ class CompactionPicker {
|
||||
protected:
|
||||
const ImmutableOptions& ioptions_;
|
||||
|
||||
// A helper function to SanitizeCompactionInputFiles() that
|
||||
// sanitizes "input_files" by adding necessary files.
|
||||
// A helper function to SanitizeAndConvertCompactionInputFiles() that
|
||||
// sanitizes "input_files" by adding necessary files.
|
||||
virtual Status SanitizeCompactionInputFilesForAllLevels(
|
||||
std::unordered_set<uint64_t>* input_files,
|
||||
const ColumnFamilyMetaData& cf_meta, const int output_level) const;
|
||||
|
||||
@@ -1651,6 +1651,31 @@ TEST_P(TimedPutPrecludeLastLevelTest, FastTrackTimedPutToLastLevel) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(TimedPutPrecludeLastLevelTest, InterleavedTimedPutAndPut) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.disable_auto_compactions = true;
|
||||
options.preclude_last_level_data_seconds = 1 * 24 * 60 * 60;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = 7;
|
||||
options.last_level_temperature = Temperature::kCold;
|
||||
options.default_write_temperature = Temperature::kHot;
|
||||
DestroyAndReopen(options);
|
||||
WriteOptions wo;
|
||||
wo.protection_bytes_per_key = GetParam();
|
||||
|
||||
// Start time: kMockStartTime = 10000000;
|
||||
ASSERT_OK(TimedPut(0, Key(0), "v0", kMockStartTime - 1 * 24 * 60 * 60, wo));
|
||||
ASSERT_OK(Put(Key(1), "v1", wo));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_EQ("0,0,0,0,0,1,1", FilesPerLevel());
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kHot), 0);
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(TimedPutPrecludeLastLevelTest, PreserveTimedPutOnPenultimateLevel) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
|
||||
+75
-22
@@ -16,6 +16,7 @@
|
||||
#include "env/mock_env.h"
|
||||
#include "port/port.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/concurrent_task_limiter.h"
|
||||
#include "rocksdb/experimental.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
@@ -6204,29 +6205,14 @@ TEST_F(DBCompactionTest, CompactionLimiter) {
|
||||
NumTableFilesAtLevel(0, 0));
|
||||
}
|
||||
|
||||
// All CFs are pending compaction
|
||||
// Wait until all CFs are pending compaction. WaitForFlushMemtable() can
|
||||
// return before the next compaction is scheduled, so we need to do some
|
||||
// waiting here.
|
||||
unsigned int tp_len = env_->GetThreadPoolQueueLen(Env::LOW);
|
||||
if (cf_count != tp_len) {
|
||||
// The test is flaky and fails the assertion below.
|
||||
// Print some debug information.
|
||||
uint64_t num_running_flushes = 0;
|
||||
if (db_->GetIntProperty(DB::Properties::kNumRunningFlushes,
|
||||
&num_running_flushes)) {
|
||||
fprintf(stdout, "Running flushes: %" PRIu64 "\n", num_running_flushes);
|
||||
}
|
||||
fprintf(stdout,
|
||||
"%zu CF in compaction queue: ", pending_compaction_cfs.size());
|
||||
for (const auto& cf_name : pending_compaction_cfs) {
|
||||
fprintf(stdout, "%s, ", cf_name.c_str());
|
||||
}
|
||||
fprintf(stdout, "\n");
|
||||
|
||||
// print lsm
|
||||
for (unsigned int cf = 0; cf < cf_count; cf++) {
|
||||
fprintf(stdout, "%s: %s\n", cf_names[cf], FilesPerLevel(cf).c_str());
|
||||
}
|
||||
for (int i = 0; i < 10000 && tp_len < cf_count; i++) {
|
||||
env_->SleepForMicroseconds(1000);
|
||||
tp_len = env_->GetThreadPoolQueueLen(Env::LOW);
|
||||
}
|
||||
|
||||
ASSERT_EQ(cf_count, tp_len);
|
||||
|
||||
// Unblock all compaction threads
|
||||
@@ -7135,7 +7121,8 @@ class DBCompactionTestWithOngoingFileIngestionParam
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ExternalSstFileIngestionJob::Run", [&](void*) {
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::CompactFilesImpl::PostSanitizeCompactionInputFiles",
|
||||
{{"DBImpl::CompactFilesImpl::"
|
||||
"PostSanitizeAndConvertCompactionInputFiles",
|
||||
"VersionSet::LogAndApply:WriteManifest"}});
|
||||
});
|
||||
} else {
|
||||
@@ -7617,6 +7604,72 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
|
||||
std::shared_ptr<test::SleepingBackgroundTask> sleeping_task_;
|
||||
};
|
||||
|
||||
TEST_F(DBCompactionTest, CompactFilesSupportKeyPlacementRangeConflict) {
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 3;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// To create LSM of below shape:
|
||||
// L0: [k2]
|
||||
// L1: [k3],[k4]
|
||||
// L2: [k1, k5]
|
||||
ASSERT_OK(Put("k1", "v"));
|
||||
ASSERT_OK(Put("k5", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
ASSERT_EQ("0,0,1", FilesPerLevel());
|
||||
|
||||
ASSERT_OK(Put("k3", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k4", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(experimental::PromoteL0(db_, db_->DefaultColumnFamily(), 1));
|
||||
ASSERT_EQ("0,2,1", FilesPerLevel());
|
||||
|
||||
ASSERT_OK(Put("k2", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ("1,2,1", FilesPerLevel());
|
||||
|
||||
Close();
|
||||
|
||||
// To force below two CompactFiles() in order to coerce range conflict on L1
|
||||
// upon (2)
|
||||
// (1): Compact [k2] at L0 and [k3] at L1 with output to L1
|
||||
// (2): Compact [k4] at L1 and [k1, k5] at L2 and output to L1 and L2
|
||||
options.preclude_last_level_data_seconds = 1;
|
||||
Reopen(options);
|
||||
|
||||
ColumnFamilyMetaData cf_meta_data;
|
||||
db_->GetColumnFamilyMetaData(&cf_meta_data);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactFilesImpl:0", [&](void* /*arg*/) {
|
||||
std::vector<std::string> c2_input_files;
|
||||
c2_input_files.push_back(cf_meta_data.levels[1].files[1].name);
|
||||
c2_input_files.push_back(cf_meta_data.levels[2].files[0].name);
|
||||
// To verify CompactFiles() is aborted upon range conflict instead
|
||||
// of crashing upon internal assertion
|
||||
Status s = db_->CompactFiles(CompactionOptions(), c2_input_files,
|
||||
2 /* output_level */);
|
||||
ASSERT_TRUE(s.IsAborted());
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::vector<std::string> c1_input_files;
|
||||
c1_input_files.push_back(cf_meta_data.levels[0].files[0].name);
|
||||
c1_input_files.push_back(cf_meta_data.levels[1].files[0].name);
|
||||
Status s = db_->CompactFiles(CompactionOptions(), c1_input_files,
|
||||
1 /* output_level */);
|
||||
ASSERT_OK(s);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTestL0FilesMisorderCorruption,
|
||||
FlushAfterIntraL0LevelCompactionWithIngestedFile) {
|
||||
SetupOptions(CompactionStyle::kCompactionStyleLevel, "");
|
||||
|
||||
+12
-6
@@ -229,12 +229,18 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
// metadata.
|
||||
mutex_.Lock();
|
||||
if (flush_memtable) {
|
||||
Status status = FlushForGetLiveFiles();
|
||||
if (!status.ok()) {
|
||||
mutex_.Unlock();
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log, "Cannot Flush data %s\n",
|
||||
status.ToString().c_str());
|
||||
return status;
|
||||
bool wal_locked = lock_wal_count_ > 0;
|
||||
if (wal_locked) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Can't FlushForGetLiveFiles while WAL is locked");
|
||||
} else {
|
||||
Status status = FlushForGetLiveFiles();
|
||||
if (!status.ok()) {
|
||||
mutex_.Unlock();
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
|
||||
"Cannot Flush data %s\n", status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+466
-2
@@ -17,6 +17,7 @@ class DBFollowerTest : public DBTestBase {
|
||||
// Create the leader DB object
|
||||
DBFollowerTest() : DBTestBase("/db_follower_test", /*env_do_fsync*/ false) {
|
||||
follower_name_ = dbname_ + "/follower";
|
||||
db_parent_ = dbname_;
|
||||
Close();
|
||||
Destroy(CurrentOptions());
|
||||
EXPECT_EQ(env_->CreateDirIfMissing(dbname_), Status::OK());
|
||||
@@ -27,17 +28,215 @@ class DBFollowerTest : public DBTestBase {
|
||||
~DBFollowerTest() {
|
||||
follower_.reset();
|
||||
EXPECT_EQ(DestroyDB(follower_name_, CurrentOptions()), Status::OK());
|
||||
Destroy(CurrentOptions());
|
||||
dbname_ = db_parent_;
|
||||
}
|
||||
|
||||
protected:
|
||||
class DBFollowerTestFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit DBFollowerTestFS(const std::shared_ptr<FileSystem>& target)
|
||||
: FileSystemWrapper(target),
|
||||
cv_(&mutex_),
|
||||
barrier_(false),
|
||||
count_(0),
|
||||
reinit_count_(0) {}
|
||||
|
||||
const char* Name() const override { return "DBFollowerTestFS"; }
|
||||
|
||||
IOStatus NewSequentialFile(const std::string& fname,
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSSequentialFile>* result,
|
||||
IODebugContext* dbg = nullptr) override {
|
||||
class DBFollowerTestSeqFile : public FSSequentialFileWrapper {
|
||||
public:
|
||||
DBFollowerTestSeqFile(DBFollowerTestFS* fs,
|
||||
std::unique_ptr<FSSequentialFile>&& file,
|
||||
uint64_t /*size*/)
|
||||
: FSSequentialFileWrapper(file.get()),
|
||||
fs_(fs),
|
||||
file_(std::move(file)) {}
|
||||
|
||||
IOStatus Read(size_t n, const IOOptions& options, Slice* result,
|
||||
char* scratch, IODebugContext* dbg) override {
|
||||
fs_->BarrierWait();
|
||||
return target()->Read(n, options, result, scratch, dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
DBFollowerTestFS* fs_;
|
||||
std::unique_ptr<FSSequentialFile> file_;
|
||||
};
|
||||
|
||||
std::unique_ptr<FSSequentialFile> file;
|
||||
IOStatus s = target()->NewSequentialFile(fname, file_opts, &file, dbg);
|
||||
|
||||
if (s.ok() && test::GetFileType(fname) == kDescriptorFile) {
|
||||
uint64_t size = 0;
|
||||
EXPECT_EQ(target()->GetFileSize(fname, IOOptions(), &size, nullptr),
|
||||
IOStatus::OK());
|
||||
result->reset(new DBFollowerTestSeqFile(this, std::move(file), size));
|
||||
} else {
|
||||
*result = std::move(file);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void BarrierInit(int count) {
|
||||
MutexLock l(&mutex_);
|
||||
barrier_ = true;
|
||||
count_ = count;
|
||||
}
|
||||
|
||||
void BarrierWait() {
|
||||
MutexLock l(&mutex_);
|
||||
if (!barrier_) {
|
||||
return;
|
||||
}
|
||||
if (--count_ == 0) {
|
||||
if (reinit_count_ > 0) {
|
||||
count_ = reinit_count_;
|
||||
reinit_count_ = 0;
|
||||
} else {
|
||||
barrier_ = false;
|
||||
}
|
||||
cv_.SignalAll();
|
||||
} else {
|
||||
cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
void BarrierWaitAndReinit(int count) {
|
||||
MutexLock l(&mutex_);
|
||||
if (!barrier_) {
|
||||
return;
|
||||
}
|
||||
reinit_count_ = count;
|
||||
if (--count_ == 0) {
|
||||
if (reinit_count_ > 0) {
|
||||
count_ = reinit_count_;
|
||||
reinit_count_ = 0;
|
||||
} else {
|
||||
barrier_ = false;
|
||||
}
|
||||
cv_.SignalAll();
|
||||
} else {
|
||||
cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
port::Mutex mutex_;
|
||||
port::CondVar cv_;
|
||||
bool barrier_;
|
||||
int count_;
|
||||
int reinit_count_;
|
||||
};
|
||||
|
||||
class DBFollowerTestSstPartitioner : public SstPartitioner {
|
||||
public:
|
||||
explicit DBFollowerTestSstPartitioner(uint64_t max_keys)
|
||||
: max_keys_(max_keys), num_keys_(0) {}
|
||||
|
||||
const char* Name() const override { return "DBFollowerTestSstPartitioner"; }
|
||||
|
||||
PartitionerResult ShouldPartition(
|
||||
const PartitionerRequest& /*request*/) override {
|
||||
if (++num_keys_ > max_keys_) {
|
||||
num_keys_ = 0;
|
||||
return PartitionerResult::kRequired;
|
||||
} else {
|
||||
return PartitionerResult::kNotRequired;
|
||||
}
|
||||
}
|
||||
|
||||
bool CanDoTrivialMove(const Slice& /*smallest_user_key*/,
|
||||
const Slice& /*largest_user_key*/) override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t max_keys_;
|
||||
uint64_t num_keys_;
|
||||
};
|
||||
|
||||
class DBFollowerTestSstPartitionerFactory : public SstPartitionerFactory {
|
||||
public:
|
||||
explicit DBFollowerTestSstPartitionerFactory(uint64_t max_keys)
|
||||
: max_keys_(max_keys) {}
|
||||
|
||||
std::unique_ptr<SstPartitioner> CreatePartitioner(
|
||||
const SstPartitioner::Context& /*context*/) const override {
|
||||
std::unique_ptr<SstPartitioner> partitioner;
|
||||
partitioner.reset(new DBFollowerTestSstPartitioner(max_keys_));
|
||||
return partitioner;
|
||||
}
|
||||
|
||||
const char* Name() const override {
|
||||
return "DBFollowerTestSstPartitionerFactory";
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t max_keys_;
|
||||
};
|
||||
|
||||
Status OpenAsFollower() {
|
||||
return DB::OpenAsFollower(CurrentOptions(), follower_name_, dbname_,
|
||||
&follower_);
|
||||
Options opts = CurrentOptions();
|
||||
if (!follower_env_) {
|
||||
follower_env_ = NewCompositeEnv(
|
||||
std::make_shared<DBFollowerTestFS>(env_->GetFileSystem()));
|
||||
}
|
||||
opts.env = follower_env_.get();
|
||||
opts.follower_refresh_catchup_period_ms = 100;
|
||||
return DB::OpenAsFollower(opts, follower_name_, dbname_, &follower_);
|
||||
}
|
||||
|
||||
std::string FollowerGet(const std::string& k) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
std::string result;
|
||||
Status s = follower()->Get(options, k, &result);
|
||||
if (s.IsNotFound()) {
|
||||
result = "NOT_FOUND";
|
||||
} else if (!s.ok()) {
|
||||
result = s.ToString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
DB* follower() { return follower_.get(); }
|
||||
DBFollowerTestFS* follower_fs() {
|
||||
return static_cast<DBFollowerTestFS*>(follower_env_->GetFileSystem().get());
|
||||
}
|
||||
|
||||
void CheckDirs() {
|
||||
std::vector<std::string> db_children;
|
||||
std::vector<std::string> follower_children;
|
||||
EXPECT_OK(env_->GetChildren(dbname_, &db_children));
|
||||
EXPECT_OK(env_->GetChildren(follower_name_, &follower_children));
|
||||
|
||||
std::set<uint64_t> db_filenums;
|
||||
std::set<uint64_t> follower_filenums;
|
||||
for (auto& name : db_children) {
|
||||
if (test::GetFileType(name) != kTableFile) {
|
||||
continue;
|
||||
}
|
||||
db_filenums.insert(test::GetFileNumber(name));
|
||||
}
|
||||
for (auto& name : follower_children) {
|
||||
if (test::GetFileType(name) != kTableFile) {
|
||||
continue;
|
||||
}
|
||||
follower_filenums.insert(test::GetFileNumber(name));
|
||||
}
|
||||
db_filenums.merge(follower_filenums);
|
||||
EXPECT_EQ(follower_filenums.size(), db_filenums.size());
|
||||
}
|
||||
|
||||
private:
|
||||
std::string follower_name_;
|
||||
std::string db_parent_;
|
||||
std::unique_ptr<Env> follower_env_;
|
||||
std::unique_ptr<DB> follower_;
|
||||
};
|
||||
|
||||
@@ -51,8 +250,273 @@ TEST_F(DBFollowerTest, Basic) {
|
||||
std::string val;
|
||||
ASSERT_OK(follower()->Get(ReadOptions(), "k1", &val));
|
||||
ASSERT_EQ(val, "v1");
|
||||
CheckDirs();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, Flush) {
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
{"Leader::Done", "DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
{"DBImplFollower::TryCatchupWithLeader:End", "Follower::WaitForCatchup"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
TEST_SYNC_POINT("Leader::Start");
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
TEST_SYNC_POINT("Leader::Done");
|
||||
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup");
|
||||
std::string val;
|
||||
ASSERT_OK(follower()->Get(ReadOptions(), "k1", &val));
|
||||
ASSERT_EQ(val, "v1");
|
||||
CheckDirs();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
// This test creates 4 L0 files, immediately followed by a compaction to L1.
|
||||
// The follower replays the 4 flush records from the MANIFEST unsuccessfully,
|
||||
// and then successfully recovers a Version from the compaction record
|
||||
TEST_F(DBFollowerTest, RetryCatchup) {
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
Reopen(opts);
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
{"DBImpl::BackgroundCompaction:Start",
|
||||
"DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
{"VersionEditHandlerPointInTime::MaybeCreateVersion:Begin1",
|
||||
"DBImpl::BackgroundCompaction:BeforeCompaction"},
|
||||
{"DBImpl::BackgroundCallCompaction:PurgedObsoleteFiles",
|
||||
"VersionEditHandlerPointInTime::MaybeCreateVersion:Begin2"},
|
||||
{"DBImplFollower::TryCatchupWithLeader:End", "Follower::WaitForCatchup"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
TEST_SYNC_POINT("Leader::Start");
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup");
|
||||
ASSERT_EQ(FollowerGet("k1"), "v4");
|
||||
CheckDirs();
|
||||
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
// This test validates the same as the previous test, except there is a
|
||||
// MANIFEST rollover between the flushes and compaction. The follower
|
||||
// does not switch to a new MANIFEST in ReadAndApply. So it would require
|
||||
// another round of refresh before catching up.
|
||||
TEST_F(DBFollowerTest, RetryCatchupManifestRollover) {
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
Reopen(opts);
|
||||
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
{"Leader::Flushed", "DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
{"VersionEditHandlerPointInTime::MaybeCreateVersion:Begin1",
|
||||
"Leader::Done"},
|
||||
{"DBImpl::BackgroundCallCompaction:PurgedObsoleteFiles",
|
||||
"VersionEditHandlerPointInTime::MaybeCreateVersion:Begin2"},
|
||||
{"DBImplFollower::TryCatchupWithLeader:End",
|
||||
"Follower::WaitForCatchup:1"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
TEST_SYNC_POINT("Leader::Start");
|
||||
ASSERT_OK(Put("k1", "v4"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
TEST_SYNC_POINT("Leader::Flushed");
|
||||
TEST_SYNC_POINT("Leader::Done");
|
||||
Reopen(opts);
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:1");
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:End",
|
||||
"Follower::WaitForCatchup:2"},
|
||||
});
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:2");
|
||||
ASSERT_EQ(FollowerGet("k1"), "v4");
|
||||
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
// This test creates 4 L0 files and compacts them. The follower, during catchup,
|
||||
// successfully instantiates 4 Versions corresponding to the 4 files (but
|
||||
// donesn't install them yet), followed by deleting those 4 and adding a new
|
||||
// file from compaction. The test verifies that the 4 L0 files are deleted
|
||||
// correctly by the follower.
|
||||
// We use teh Barrier* functions to ensure that the follower first sees the 4
|
||||
// L0 files and is able to link them, and then sees the compaction that
|
||||
// obsoletes those L0 files (so those L0 files are intermediates that it has
|
||||
// to explicitly delete). Suppose we don't have any barriers, its possible
|
||||
// the follower reads the L0 records and compaction records from the MANIFEST
|
||||
// in one read, which means those L0 files would have already been deleted
|
||||
// by the leader and the follower cannot link to them.
|
||||
TEST_F(DBFollowerTest, IntermediateObsoleteFiles) {
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
Reopen(opts);
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
|
||||
follower_fs()->BarrierInit(2);
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k1", "v4"));
|
||||
ASSERT_OK(Flush());
|
||||
follower_fs()->BarrierWaitAndReinit(2);
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
follower_fs()->BarrierWait();
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:End",
|
||||
"Follower::WaitForCatchup:1"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:1");
|
||||
CheckDirs();
|
||||
ASSERT_EQ(FollowerGet("k1"), "v4");
|
||||
}
|
||||
|
||||
// This test verifies a scenario where the follower can recover a Version
|
||||
// partially (i.e some of the additions cannot be found), and the files
|
||||
// that are found are obsoleted by a subsequent VersionEdit.
|
||||
TEST_F(DBFollowerTest, PartialVersionRecovery) {
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
opts.sst_partitioner_factory =
|
||||
std::make_shared<DBFollowerTestSstPartitionerFactory>(1);
|
||||
Reopen(opts);
|
||||
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Put("k2", "v1"));
|
||||
ASSERT_OK(Put("k3", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2);
|
||||
|
||||
ASSERT_OK(Put("k1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k3", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
ASSERT_OK(dbfull()->SetOptions(dbfull()->DefaultColumnFamily(),
|
||||
{{"max_compaction_bytes", "1"}}));
|
||||
|
||||
follower_fs()->BarrierInit(2);
|
||||
Slice key("k1");
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(1, &key, &key, nullptr, true));
|
||||
|
||||
follower_fs()->BarrierWaitAndReinit(2);
|
||||
|
||||
// The second compaction input overlaps the previous compaction outputs
|
||||
// by one file. This file is never added to VersionStorageInfo since it
|
||||
// was added and deleted before the catch up completes. We later verify that
|
||||
// the follower correctly deleted this file.
|
||||
key = Slice("k3");
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(1, &key, &key, nullptr, true));
|
||||
follower_fs()->BarrierWait();
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:End",
|
||||
"Follower::WaitForCatchup:1"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:1");
|
||||
CheckDirs();
|
||||
ASSERT_EQ(FollowerGet("k1"), "v2");
|
||||
ASSERT_EQ(FollowerGet("k2"), "v1");
|
||||
ASSERT_EQ(FollowerGet("k3"), "v2");
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
// This test verifies a scenario similar to the PartialVersionRecovery, except
|
||||
// with a MANIFEST rollover in between. When there is a rollover, the
|
||||
// follower's attempt ends without installing a new Version. The next catch up
|
||||
// attempt will recover a full Version.
|
||||
TEST_F(DBFollowerTest, PartialVersionRecoveryWithRollover) {
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
opts.sst_partitioner_factory =
|
||||
std::make_shared<DBFollowerTestSstPartitionerFactory>(1);
|
||||
Reopen(opts);
|
||||
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Put("k2", "v1"));
|
||||
ASSERT_OK(Put("k3", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2);
|
||||
|
||||
ASSERT_OK(Put("k1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k3", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
opts.max_compaction_bytes = 1;
|
||||
Reopen(opts);
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
|
||||
follower_fs()->BarrierInit(2);
|
||||
Slice key("k1");
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(1, &key, &key, nullptr, true));
|
||||
|
||||
follower_fs()->BarrierWaitAndReinit(2);
|
||||
Reopen(opts);
|
||||
key = Slice("k3");
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(1, &key, &key, nullptr, true));
|
||||
follower_fs()->BarrierWait();
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1",
|
||||
"Follower::WaitForCatchup:1"},
|
||||
{"Follower::WaitForCatchup:2",
|
||||
"DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:1");
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:2");
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:End",
|
||||
"Follower::WaitForCatchup:3"},
|
||||
});
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup:3");
|
||||
CheckDirs();
|
||||
ASSERT_EQ(FollowerGet("k1"), "v2");
|
||||
ASSERT_EQ(FollowerGet("k2"), "v1");
|
||||
ASSERT_EQ(FollowerGet("k3"), "v2");
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
#endif
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
+143
-38
@@ -17,6 +17,7 @@
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
@@ -2099,7 +2100,7 @@ Status DBImpl::GetEntity(const ReadOptions& _read_options,
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetEntity with `ReadOptions::io_activity` != "
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
@@ -2126,7 +2127,7 @@ Status DBImpl::GetEntity(const ReadOptions& _read_options, const Slice& key,
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
s = Status::InvalidArgument(
|
||||
"Cannot call GetEntity with `ReadOptions::io_activity` != "
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].SetStatus(s);
|
||||
@@ -3185,22 +3186,55 @@ void DBImpl::MultiGetEntity(const ReadOptions& _read_options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableWideColumns* results,
|
||||
Status* statuses, bool sorted_input) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
|
||||
Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntity with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
|
||||
assert(statuses);
|
||||
|
||||
if (!column_families) {
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntity without column families");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keys) {
|
||||
const Status s =
|
||||
Status::InvalidArgument("Cannot call MultiGetEntity without keys");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntity without PinnableWideColumns objects");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGetEntity;
|
||||
}
|
||||
|
||||
MultiGetCommon(read_options, num_keys, column_families, keys,
|
||||
/* values */ nullptr, results, /* timestamps */ nullptr,
|
||||
statuses, sorted_input);
|
||||
@@ -3210,22 +3244,54 @@ void DBImpl::MultiGetEntity(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, size_t num_keys,
|
||||
const Slice* keys, PinnableWideColumns* results,
|
||||
Status* statuses, bool sorted_input) {
|
||||
assert(statuses);
|
||||
|
||||
if (!column_family) {
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntity without a column family handle");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keys) {
|
||||
const Status s =
|
||||
Status::InvalidArgument("Cannot call MultiGetEntity without keys");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntity without PinnableWideColumns objects");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
|
||||
Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntity with `ReadOptions::io_activity` is "
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
statuses[i] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGetEntity;
|
||||
}
|
||||
|
||||
MultiGetCommon(read_options, column_family, num_keys, keys,
|
||||
/* values */ nullptr, results, /* timestamps */ nullptr,
|
||||
statuses, sorted_input);
|
||||
@@ -3234,18 +3300,34 @@ void DBImpl::MultiGetEntity(const ReadOptions& _read_options,
|
||||
void DBImpl::MultiGetEntity(const ReadOptions& _read_options, size_t num_keys,
|
||||
const Slice* keys,
|
||||
PinnableAttributeGroups* results) {
|
||||
assert(results);
|
||||
|
||||
if (!keys) {
|
||||
const Status s =
|
||||
Status::InvalidArgument("Cannot call MultiGetEntity without keys");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
for (size_t j = 0; j < results[i].size(); ++j) {
|
||||
results[i][j].SetStatus(s);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
|
||||
Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntity with ReadOptions::io_activity` is "
|
||||
const Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
for (size_t j = 0; j < results[i].size(); ++j) {
|
||||
results[i][j].SetStatus(s);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGetEntity;
|
||||
@@ -3263,6 +3345,7 @@ void DBImpl::MultiGetEntity(const ReadOptions& _read_options, size_t num_keys,
|
||||
++total_count;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Status> statuses(total_count);
|
||||
std::vector<PinnableWideColumns> columns(total_count);
|
||||
MultiGetCommon(read_options, total_count, column_families.data(),
|
||||
@@ -3283,6 +3366,15 @@ void DBImpl::MultiGetEntity(const ReadOptions& _read_options, size_t num_keys,
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetEntityWithCallback(
|
||||
const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys) {
|
||||
assert(read_options.io_activity == Env::IOActivity::kMultiGetEntity);
|
||||
|
||||
MultiGetWithCallbackImpl(read_options, column_family, callback, sorted_keys);
|
||||
}
|
||||
|
||||
Status DBImpl::WrapUpCreateColumnFamilies(
|
||||
const ReadOptions& read_options, const WriteOptions& write_options,
|
||||
const std::vector<const ColumnFamilyOptions*>& cf_options) {
|
||||
@@ -5072,6 +5164,14 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
Env* env = soptions.env;
|
||||
std::vector<std::string> filenames;
|
||||
bool wal_in_db_path = soptions.IsWalDirSameAsDBPath();
|
||||
auto sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
options.sst_file_manager.get());
|
||||
// Allocate a separate trash bucket to be used by all the to be deleted
|
||||
// files, so we can later wait for this bucket to be empty before return.
|
||||
std::optional<int32_t> bucket;
|
||||
if (sfm) {
|
||||
bucket = sfm->NewTrashBucket();
|
||||
}
|
||||
|
||||
// Reset the logger because it holds a handle to the
|
||||
// log file and prevents cleanup and directory removal
|
||||
@@ -5083,6 +5183,7 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
/*IODebugContext*=*/nullptr)
|
||||
.PermitUncheckedError();
|
||||
|
||||
std::set<std::string> paths_to_delete;
|
||||
FileLock* lock;
|
||||
const std::string lockname = LockFileName(dbname);
|
||||
Status result = env->LockFile(lockname, &lock);
|
||||
@@ -5099,10 +5200,9 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
del = DestroyDB(path_to_delete, options);
|
||||
} else if (type == kTableFile || type == kWalFile ||
|
||||
type == kBlobFile) {
|
||||
del = DeleteDBFile(
|
||||
&soptions, path_to_delete, dbname,
|
||||
/*force_bg=*/false,
|
||||
/*force_fg=*/(type == kWalFile) ? !wal_in_db_path : false);
|
||||
del = DeleteUnaccountedDBFile(&soptions, path_to_delete, dbname,
|
||||
/*force_bg=*/false,
|
||||
/*force_fg=*/false, bucket);
|
||||
} else {
|
||||
del = env->DeleteFile(path_to_delete);
|
||||
}
|
||||
@@ -5111,6 +5211,7 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
}
|
||||
}
|
||||
}
|
||||
paths_to_delete.insert(dbname);
|
||||
|
||||
std::set<std::string> paths;
|
||||
for (const DbPath& db_path : options.db_paths) {
|
||||
@@ -5132,18 +5233,19 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
(type == kTableFile ||
|
||||
type == kBlobFile)) { // Lock file will be deleted at end
|
||||
std::string file_path = path + "/" + fname;
|
||||
Status del = DeleteDBFile(&soptions, file_path, dbname,
|
||||
/*force_bg=*/false, /*force_fg=*/false);
|
||||
Status del = DeleteUnaccountedDBFile(&soptions, file_path, dbname,
|
||||
/*force_bg=*/false,
|
||||
/*force_fg=*/false, bucket);
|
||||
if (!del.ok() && result.ok()) {
|
||||
result = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Should we return an error if we cannot delete the directory?
|
||||
env->DeleteDir(path).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
paths_to_delete.merge(paths);
|
||||
|
||||
std::vector<std::string> walDirFiles;
|
||||
std::string archivedir = ArchivalDirectory(dbname);
|
||||
bool wal_dir_exists = false;
|
||||
@@ -5167,46 +5269,49 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
// Delete archival files.
|
||||
for (const auto& file : archiveFiles) {
|
||||
if (ParseFileName(file, &number, &type) && type == kWalFile) {
|
||||
Status del =
|
||||
DeleteDBFile(&soptions, archivedir + "/" + file, archivedir,
|
||||
/*force_bg=*/false, /*force_fg=*/!wal_in_db_path);
|
||||
Status del = DeleteUnaccountedDBFile(
|
||||
&soptions, archivedir + "/" + file, archivedir,
|
||||
/*force_bg=*/false, /*force_fg=*/!wal_in_db_path, bucket);
|
||||
if (!del.ok() && result.ok()) {
|
||||
result = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore error in case dir contains other files
|
||||
env->DeleteDir(archivedir).PermitUncheckedError();
|
||||
paths_to_delete.insert(archivedir);
|
||||
}
|
||||
|
||||
// Delete log files in the WAL dir
|
||||
if (wal_dir_exists) {
|
||||
for (const auto& file : walDirFiles) {
|
||||
if (ParseFileName(file, &number, &type) && type == kWalFile) {
|
||||
Status del =
|
||||
DeleteDBFile(&soptions, LogFileName(soptions.wal_dir, number),
|
||||
soptions.wal_dir, /*force_bg=*/false,
|
||||
/*force_fg=*/!wal_in_db_path);
|
||||
Status del = DeleteUnaccountedDBFile(
|
||||
&soptions, LogFileName(soptions.wal_dir, number),
|
||||
soptions.wal_dir, /*force_bg=*/false,
|
||||
/*force_fg=*/!wal_in_db_path, bucket);
|
||||
if (!del.ok() && result.ok()) {
|
||||
result = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore error in case dir contains other files
|
||||
env->DeleteDir(soptions.wal_dir).PermitUncheckedError();
|
||||
paths_to_delete.insert(soptions.wal_dir);
|
||||
}
|
||||
|
||||
// Ignore error since state is already gone
|
||||
env->UnlockFile(lock).PermitUncheckedError();
|
||||
env->DeleteFile(lockname).PermitUncheckedError();
|
||||
|
||||
// Make sure trash files are all cleared before return.
|
||||
if (sfm && bucket.has_value()) {
|
||||
sfm->WaitForEmptyTrashBucket(bucket.value());
|
||||
}
|
||||
// sst_file_manager holds a ref to the logger. Make sure the logger is
|
||||
// gone before trying to remove the directory.
|
||||
soptions.sst_file_manager.reset();
|
||||
|
||||
// Ignore error in case dir contains other files
|
||||
env->DeleteDir(dbname).PermitUncheckedError();
|
||||
;
|
||||
for (const auto& path_to_delete : paths_to_delete) {
|
||||
env->DeleteDir(path_to_delete).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+24
-11
@@ -291,6 +291,11 @@ class DBImpl : public DB {
|
||||
const Slice* keys,
|
||||
PinnableAttributeGroups* results) override;
|
||||
|
||||
void MultiGetEntityWithCallback(
|
||||
const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys);
|
||||
|
||||
Status CreateColumnFamily(const ColumnFamilyOptions& cf_options,
|
||||
const std::string& column_family,
|
||||
ColumnFamilyHandle** handle) override {
|
||||
@@ -1206,6 +1211,8 @@ class DBImpl : public DB {
|
||||
return logs_.back().number;
|
||||
}
|
||||
|
||||
void TEST_DeleteObsoleteFiles();
|
||||
|
||||
const std::unordered_set<uint64_t>& TEST_GetFilesGrabbedForPurge() const {
|
||||
return files_grabbed_for_purge_;
|
||||
}
|
||||
@@ -1643,6 +1650,7 @@ class DBImpl : public DB {
|
||||
friend class ForwardIterator;
|
||||
friend struct SuperVersion;
|
||||
friend class CompactedDBImpl;
|
||||
friend class DBImplFollower;
|
||||
#ifndef NDEBUG
|
||||
friend class DBTest_ConcurrentFlushWAL_Test;
|
||||
friend class DBTest_MixedSlowdownOptionsStop_Test;
|
||||
@@ -2048,17 +2056,22 @@ class DBImpl : public DB {
|
||||
mutex_.Lock();
|
||||
}
|
||||
|
||||
if (!immutable_db_options_.unordered_write) {
|
||||
// Then the writes are finished before the next write group starts
|
||||
return;
|
||||
if (immutable_db_options_.unordered_write) {
|
||||
// Wait for the ones who already wrote to the WAL to finish their
|
||||
// memtable write.
|
||||
if (pending_memtable_writes_.load() != 0) {
|
||||
// XXX: suspicious wait while holding DB mutex?
|
||||
std::unique_lock<std::mutex> guard(switch_mutex_);
|
||||
switch_cv_.wait(guard,
|
||||
[&] { return pending_memtable_writes_.load() == 0; });
|
||||
}
|
||||
} else {
|
||||
// (Writes are finished before the next write group starts.)
|
||||
}
|
||||
|
||||
// Wait for the ones who already wrote to the WAL to finish their
|
||||
// memtable write.
|
||||
if (pending_memtable_writes_.load() != 0) {
|
||||
std::unique_lock<std::mutex> guard(switch_mutex_);
|
||||
switch_cv_.wait(guard,
|
||||
[&] { return pending_memtable_writes_.load() == 0; });
|
||||
// Wait for any LockWAL to clear
|
||||
while (lock_wal_count_ > 0) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2987,9 +3000,9 @@ inline Status DBImpl::FailIfReadCollapsedHistory(const ColumnFamilyData* cfd,
|
||||
if (!full_history_ts_low.empty() &&
|
||||
ucmp->CompareTimestamp(ts, full_history_ts_low) < 0) {
|
||||
std::stringstream oss;
|
||||
oss << "Read timestamp: " << ts.ToString(true)
|
||||
oss << "Read timestamp: " << ucmp->TimestampToString(ts)
|
||||
<< " is smaller than full_history_ts_low: "
|
||||
<< Slice(full_history_ts_low).ToString(true) << std::endl;
|
||||
<< ucmp->TimestampToString(full_history_ts_low) << std::endl;
|
||||
return Status::InvalidArgument(oss.str());
|
||||
}
|
||||
return Status::OK();
|
||||
|
||||
@@ -224,9 +224,15 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
// the host crashes after flushing and before WAL is persistent, the
|
||||
// flushed SST may contain data from write batches whose updates to
|
||||
// other (unflushed) column families are missing.
|
||||
//
|
||||
// When 2PC is enabled, non-recent WAL(s) may be needed for crash-recovery,
|
||||
// even when there is only one CF in the DB, for prepared transactions that
|
||||
// had not been committed yet. Make sure we sync them to keep the persisted
|
||||
// WAL state at least as new as the persisted SST state.
|
||||
const bool needs_to_sync_closed_wals =
|
||||
logfile_number_ > 0 &&
|
||||
versions_->GetColumnFamilySet()->NumberOfColumnFamilies() > 1;
|
||||
(versions_->GetColumnFamilySet()->NumberOfColumnFamilies() > 1 ||
|
||||
allow_2pc());
|
||||
|
||||
// If needs_to_sync_closed_wals is true, we need to record the current
|
||||
// maximum memtable ID of this column family so that a later PickMemtables()
|
||||
@@ -1088,7 +1094,12 @@ Status DBImpl::IncreaseFullHistoryTsLowImpl(ColumnFamilyData* cfd,
|
||||
assert(ucmp->timestamp_size() == ts_low.size() && !ts_low.empty());
|
||||
if (!current_ts_low.empty() &&
|
||||
ucmp->CompareTimestamp(ts_low, current_ts_low) < 0) {
|
||||
return Status::InvalidArgument("Cannot decrease full_history_ts_low");
|
||||
std::stringstream oss;
|
||||
oss << "Current full_history_ts_low: "
|
||||
<< ucmp->TimestampToString(current_ts_low)
|
||||
<< " is higher than provided ts: " << ucmp->TimestampToString(ts_low)
|
||||
<< std::endl;
|
||||
return Status::InvalidArgument(oss.str());
|
||||
}
|
||||
|
||||
Status s = versions_->LogAndApply(cfd, *cfd->GetLatestMutableCFOptions(),
|
||||
@@ -1120,6 +1131,11 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
if (options.target_path_id >= cfd->ioptions()->cf_paths.size()) {
|
||||
return Status::InvalidArgument("Invalid target path ID");
|
||||
}
|
||||
if (options.change_level &&
|
||||
cfd->ioptions()->compaction_style == kCompactionStyleFIFO) {
|
||||
return Status::NotSupported(
|
||||
"FIFO compaction does not support change_level.");
|
||||
}
|
||||
|
||||
bool flush_needed = true;
|
||||
|
||||
@@ -1180,6 +1196,16 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
final_output_level, options, begin, end, exclusive,
|
||||
false /* disable_trivial_move */,
|
||||
std::numeric_limits<uint64_t>::max(), trim_ts);
|
||||
} else if (cfd->ioptions()->compaction_style == kCompactionStyleFIFO) {
|
||||
// FIFOCompactionPicker::CompactRange() will ignore the input key range
|
||||
// [begin, end] and just try to pick compaction based on the configured
|
||||
// option `compaction_options_fifo`. So we skip checking if [begin, end]
|
||||
// overlaps with the DB here.
|
||||
final_output_level = 0;
|
||||
s = RunManualCompaction(cfd, /*input_level=*/0, final_output_level, options,
|
||||
begin, end, exclusive,
|
||||
false /* disable_trivial_move */,
|
||||
std::numeric_limits<uint64_t>::max(), trim_ts);
|
||||
} else {
|
||||
int first_overlapped_level = kInvalidLevel;
|
||||
{
|
||||
@@ -1264,8 +1290,7 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
CleanupSuperVersion(super_version);
|
||||
}
|
||||
if (s.ok() && first_overlapped_level != kInvalidLevel) {
|
||||
if (cfd->ioptions()->compaction_style == kCompactionStyleUniversal ||
|
||||
cfd->ioptions()->compaction_style == kCompactionStyleFIFO) {
|
||||
if (cfd->ioptions()->compaction_style == kCompactionStyleUniversal) {
|
||||
assert(first_overlapped_level == 0);
|
||||
s = RunManualCompaction(
|
||||
cfd, first_overlapped_level, first_overlapped_level, options, begin,
|
||||
@@ -1517,16 +1542,11 @@ Status DBImpl::CompactFilesImpl(
|
||||
std::to_string(cfd->ioptions()->num_levels - 1));
|
||||
}
|
||||
|
||||
Status s = cfd->compaction_picker()->SanitizeCompactionInputFiles(
|
||||
&input_set, cf_meta, output_level);
|
||||
TEST_SYNC_POINT("DBImpl::CompactFilesImpl::PostSanitizeCompactionInputFiles");
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
s = cfd->compaction_picker()->GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, version->storage_info(), compact_options);
|
||||
Status s = cfd->compaction_picker()->SanitizeAndConvertCompactionInputFiles(
|
||||
&input_set, cf_meta, output_level, version->storage_info(), &input_files);
|
||||
TEST_SYNC_POINT(
|
||||
"DBImpl::CompactFilesImpl::PostSanitizeAndConvertCompactionInputFiles");
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3340,7 +3360,7 @@ void DBImpl::BackgroundCallFlush(Env::Priority thread_pri) {
|
||||
bg_cv_.SignalAll(); // In case a waiter can proceed despite the error
|
||||
mutex_.Unlock();
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
|
||||
"[JOB %d] Waiting after background flush error: %s"
|
||||
"[JOB %d] Waiting after background flush error: %s, "
|
||||
"Accumulated background error counts: %" PRIu64,
|
||||
job_context.job_id, s.ToString().c_str(), error_cnt);
|
||||
log_buffer.FlushBufferToLog();
|
||||
|
||||
@@ -314,6 +314,11 @@ const autovector<uint64_t>& DBImpl::TEST_GetFilesToQuarantine() const {
|
||||
return error_handler_.GetFilesToQuarantine();
|
||||
}
|
||||
|
||||
void DBImpl::TEST_DeleteObsoleteFiles() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
DeleteObsoleteFiles();
|
||||
}
|
||||
|
||||
size_t DBImpl::TEST_EstimateInMemoryStatsHistorySize() const {
|
||||
InstrumentedMutexLock l(&const_cast<DBImpl*>(this)->stats_history_mutex_);
|
||||
return EstimateInMemoryStatsHistorySize();
|
||||
|
||||
@@ -95,17 +95,28 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
assert(versions_.get() != nullptr);
|
||||
assert(manifest_reader_.get() != nullptr);
|
||||
Status s;
|
||||
|
||||
TEST_SYNC_POINT("DBImplFollower::TryCatchupWithLeader:Begin1");
|
||||
TEST_SYNC_POINT("DBImplFollower::TryCatchupWithLeader:Begin2");
|
||||
// read the manifest and apply new changes to the follower instance
|
||||
std::unordered_set<ColumnFamilyData*> cfds_changed;
|
||||
JobContext job_context(0, true /*create_superversion*/);
|
||||
{
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
std::vector<std::string> files_to_delete;
|
||||
s = static_cast_with_check<ReactiveVersionSet>(versions_.get())
|
||||
->ReadAndApply(&mutex_, &manifest_reader_,
|
||||
manifest_reader_status_.get(), &cfds_changed);
|
||||
manifest_reader_status_.get(), &cfds_changed,
|
||||
&files_to_delete);
|
||||
ReleaseFileNumberFromPendingOutputs(pending_outputs_inserted_elem_);
|
||||
pending_outputs_inserted_elem_.reset(new std::list<uint64_t>::iterator(
|
||||
CaptureCurrentFileNumberInPendingOutputs()));
|
||||
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log, "Last sequence is %" PRIu64,
|
||||
static_cast<uint64_t>(versions_->LastSequence()));
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log, "Next file number is %" PRIu64,
|
||||
static_cast<uint64_t>(versions_->current_next_file_number()));
|
||||
for (ColumnFamilyData* cfd : cfds_changed) {
|
||||
if (cfd->IsDropped()) {
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log, "[%s] is dropped\n",
|
||||
@@ -147,9 +158,33 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
sv_context.NewSuperVersion();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& file : files_to_delete) {
|
||||
IOStatus io_s = fs_->DeleteFile(file, IOOptions(), nullptr);
|
||||
if (!io_s.ok()) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Cannot delete file %s: %s", file.c_str(),
|
||||
io_s.ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
job_context.Clean();
|
||||
|
||||
// Cleanup unused, obsolete files.
|
||||
JobContext purge_files_job_context(0);
|
||||
{
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
// Currently, follower instance does not create any database files, thus
|
||||
// is unnecessary for the follower to force full scan.
|
||||
FindObsoleteFiles(&purge_files_job_context, /*force=*/false);
|
||||
}
|
||||
if (purge_files_job_context.HaveSomethingToDelete()) {
|
||||
PurgeObsoleteFiles(purge_files_job_context);
|
||||
}
|
||||
purge_files_job_context.Clean();
|
||||
|
||||
TEST_SYNC_POINT("DBImplFollower::TryCatchupWithLeader:End");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -199,6 +234,8 @@ Status DBImplFollower::Close() {
|
||||
catch_up_thread_.reset();
|
||||
}
|
||||
|
||||
ReleaseFileNumberFromPendingOutputs(pending_outputs_inserted_elem_);
|
||||
|
||||
return DBImpl::Close();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class DBImplFollower : public DBImplSecondary {
|
||||
bool OwnTablesAndLogs() const override {
|
||||
// TODO: Change this to true once we've properly implemented file
|
||||
// deletion for the read scaling case
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Status Recover(const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
@@ -49,5 +49,6 @@ class DBImplFollower : public DBImplSecondary {
|
||||
std::string src_path_;
|
||||
port::Mutex mu_;
|
||||
port::CondVar cv_;
|
||||
std::unique_ptr<std::list<uint64_t>::iterator> pending_outputs_inserted_elem_;
|
||||
};
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -680,7 +680,8 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
s = static_cast_with_check<ReactiveVersionSet>(versions_.get())
|
||||
->ReadAndApply(&mutex_, &manifest_reader_,
|
||||
manifest_reader_status_.get(), &cfds_changed);
|
||||
manifest_reader_status_.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr);
|
||||
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log, "Last sequence is %" PRIu64,
|
||||
static_cast<uint64_t>(versions_->LastSequence()));
|
||||
|
||||
@@ -221,7 +221,11 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
return Status::InvalidArgument(
|
||||
"`WriteOptions::protection_bytes_per_key` must be zero or eight");
|
||||
} else if (write_options.disableWAL &&
|
||||
immutable_db_options_.recycle_log_file_num > 0) {
|
||||
immutable_db_options_.recycle_log_file_num > 0 &&
|
||||
!(two_write_queues_ && disable_memtable)) {
|
||||
// Corruption detection in recycled WALs relies on sequential sequence
|
||||
// numbers, but WritePreparedTxnDB uses disableWAL internally for split
|
||||
// writes
|
||||
return Status::InvalidArgument(
|
||||
"WriteOptions::disableWAL option is not supported if "
|
||||
"DBOptions::recycle_log_file_num > 0");
|
||||
@@ -2152,6 +2156,8 @@ void DBImpl::NotifyOnMemTableSealed(ColumnFamilyData* /*cfd*/,
|
||||
// two_write_queues_ is true (This is to simplify the reasoning.)
|
||||
Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context) {
|
||||
mutex_.AssertHeld();
|
||||
assert(lock_wal_count_ == 0);
|
||||
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const ReadOptions read_options;
|
||||
const WriteOptions write_options;
|
||||
|
||||
@@ -109,6 +109,16 @@ Status DBIter::GetProperty(std::string prop_name, std::string* prop) {
|
||||
*prop = "Iterator is not valid.";
|
||||
}
|
||||
return Status::OK();
|
||||
} else if (prop_name == "rocksdb.iterator.is-value-pinned") {
|
||||
if (valid_) {
|
||||
*prop = (pin_thru_lifetime_ && iter_.Valid() &&
|
||||
iter_.value().data() == value_.data())
|
||||
? "1"
|
||||
: "0";
|
||||
} else {
|
||||
*prop = "Iterator is not valid.";
|
||||
}
|
||||
return Status::OK();
|
||||
} else if (prop_name == "rocksdb.iterator.internal-key") {
|
||||
*prop = saved_key_.GetUserKey().ToString();
|
||||
return Status::OK();
|
||||
|
||||
+19
-4
@@ -133,11 +133,17 @@ TEST_P(DBIteratorTest, IteratorProperty) {
|
||||
ASSERT_NOK(iter->GetProperty("non_existing.value", &prop_value));
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("0", prop_value);
|
||||
ASSERT_OK(
|
||||
iter->GetProperty("rocksdb.iterator.is-value-pinned", &prop_value));
|
||||
ASSERT_EQ("0", prop_value);
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.internal-key", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
iter->Next();
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("Iterator is not valid.", prop_value);
|
||||
ASSERT_OK(
|
||||
iter->GetProperty("rocksdb.iterator.is-value-pinned", &prop_value));
|
||||
ASSERT_EQ("Iterator is not valid.", prop_value);
|
||||
|
||||
// Get internal key at which the iteration stopped (tombstone in this case).
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.internal-key", &prop_value));
|
||||
@@ -1680,12 +1686,15 @@ TEST_P(DBIteratorTest, PinnedDataIteratorMultipleFiles) {
|
||||
ro.pin_data = true;
|
||||
auto iter = NewIterator(ro);
|
||||
|
||||
std::vector<std::pair<Slice, std::string>> results;
|
||||
std::vector<std::pair<Slice, Slice>> results;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
std::string prop_value;
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
results.emplace_back(iter->key(), iter->value().ToString());
|
||||
ASSERT_OK(
|
||||
iter->GetProperty("rocksdb.iterator.is-value-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
results.emplace_back(iter->key(), iter->value());
|
||||
}
|
||||
|
||||
ASSERT_EQ(results.size(), true_data.size());
|
||||
@@ -1739,6 +1748,9 @@ TEST_P(DBIteratorTest, PinnedDataIteratorMergeOperator) {
|
||||
std::string prop_value;
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
ASSERT_OK(
|
||||
iter->GetProperty("rocksdb.iterator.is-value-pinned", &prop_value));
|
||||
ASSERT_EQ("0", prop_value);
|
||||
results.emplace_back(iter->key(), iter->value().ToString());
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
@@ -1792,12 +1804,15 @@ TEST_P(DBIteratorTest, PinnedDataIteratorReadAfterUpdate) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<Slice, std::string>> results;
|
||||
std::vector<std::pair<Slice, Slice>> results;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
std::string prop_value;
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
results.emplace_back(iter->key(), iter->value().ToString());
|
||||
ASSERT_OK(
|
||||
iter->GetProperty("rocksdb.iterator.is-value-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
results.emplace_back(iter->key(), iter->value());
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
|
||||
@@ -732,7 +732,7 @@ TEST_P(DbMemtableKVChecksumTest, GetWithCorruptAfterMemtableInsert) {
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Memtable::SaveValue:Begin:entry", [&](void* entry) {
|
||||
"Memtable::SaveValue:Found:entry", [&](void* entry) {
|
||||
char* buf = *static_cast<char**>(entry);
|
||||
buf[corrupt_byte_offset_] += corrupt_byte_addend_;
|
||||
++corrupt_byte_offset_;
|
||||
@@ -769,7 +769,7 @@ TEST_P(DbMemtableKVChecksumTest,
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Memtable::SaveValue:Begin:entry", [&](void* entry) {
|
||||
"Memtable::SaveValue:Found:entry", [&](void* entry) {
|
||||
char* buf = *static_cast<char**>(entry);
|
||||
buf[corrupt_byte_offset_] += corrupt_byte_addend_;
|
||||
++corrupt_byte_offset_;
|
||||
|
||||
+53
-1
@@ -507,6 +507,23 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFiles) {
|
||||
ASSERT_EQ(files_deleted, 0);
|
||||
ASSERT_EQ(files_scheduled_to_delete, 0);
|
||||
Close();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion", [&](void* arg) {
|
||||
assert(arg);
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
++files_scheduled_to_delete;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::OnDeleteFile", [&](void* arg) {
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
files_deleted++;
|
||||
}
|
||||
});
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_EQ(files_deleted, blob_files.size());
|
||||
ASSERT_EQ(files_scheduled_to_delete, blob_files.size());
|
||||
@@ -649,6 +666,23 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFilesWithGC) {
|
||||
}
|
||||
|
||||
Close();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion", [&](void* arg) {
|
||||
assert(arg);
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
++files_scheduled_to_delete;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::OnDeleteFile", [&](void* arg) {
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
files_deleted++;
|
||||
}
|
||||
});
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
sfm->WaitForEmptyTrash();
|
||||
ASSERT_EQ(files_deleted, 5);
|
||||
@@ -887,7 +921,7 @@ TEST_P(DBWALTestWithParam, WALTrashCleanupOnOpen) {
|
||||
// before restarting the DB.
|
||||
// We have to set this on the 2nd to last file for it to delay deletion
|
||||
// on the last file. (Quirk of DeleteScheduler::BackgroundEmptyTrash())
|
||||
options.sst_file_manager->SetDeleteRateBytesPerSecond(1);
|
||||
options.sst_file_manager->SetDeleteRateBytesPerSecond(1024 * 1024);
|
||||
}
|
||||
ASSERT_OK(Put("Key2", DummyString(1024, v)));
|
||||
ASSERT_OK(Put("Key3", DummyString(1024, v)));
|
||||
@@ -1902,6 +1936,24 @@ TEST_F(DBSSTTest, DBWithSFMForBlobFilesAtomicFlush) {
|
||||
ASSERT_EQ(files_deleted, 1);
|
||||
|
||||
Close();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion", [&](void* arg) {
|
||||
assert(arg);
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
++files_scheduled_to_delete;
|
||||
}
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::OnDeleteFile", [&](void* arg) {
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
files_deleted++;
|
||||
}
|
||||
});
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
|
||||
ASSERT_EQ(files_scheduled_to_delete, 4);
|
||||
|
||||
+25
-17
@@ -3844,6 +3844,9 @@ TEST_P(DBTestWithParam, FIFOCompactionTest) {
|
||||
} else {
|
||||
CompactRangeOptions cro;
|
||||
cro.exclusive_manual_compaction = exclusive_manual_compaction_;
|
||||
cro.change_level = true;
|
||||
ASSERT_TRUE(db_->CompactRange(cro, nullptr, nullptr).IsNotSupported());
|
||||
cro.change_level = false;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
// only 5 files should survive
|
||||
@@ -4674,24 +4677,27 @@ TEST_F(DBTest, DynamicMemtableOptions) {
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
namespace {
|
||||
void VerifyOperationCount(Env* env, ThreadStatus::OperationType op_type,
|
||||
bool VerifyOperationCount(Env* env, ThreadStatus::OperationType op_type,
|
||||
int expected_count) {
|
||||
int op_count = 0;
|
||||
std::vector<ThreadStatus> thread_list;
|
||||
ASSERT_OK(env->GetThreadList(&thread_list));
|
||||
EXPECT_OK(env->GetThreadList(&thread_list));
|
||||
for (const auto& thread : thread_list) {
|
||||
if (thread.operation_type == op_type) {
|
||||
op_count++;
|
||||
}
|
||||
}
|
||||
if (op_count != expected_count) {
|
||||
fprintf(stderr, "op_count: %d, expected_count %d\n", op_count,
|
||||
expected_count);
|
||||
for (const auto& thread : thread_list) {
|
||||
fprintf(stderr, "thread id: %" PRIu64 ", thread status: %s\n",
|
||||
fprintf(stderr, "thread id: %" PRIu64 ", thread status: %s, cf_name %s\n",
|
||||
thread.thread_id,
|
||||
thread.GetOperationName(thread.operation_type).c_str());
|
||||
thread.GetOperationName(thread.operation_type).c_str(),
|
||||
thread.cf_name.c_str());
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(op_count, expected_count);
|
||||
return op_count == expected_count;
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
@@ -4791,11 +4797,11 @@ TEST_F(DBTest, ThreadStatusFlush) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
VerifyOperationCount(env_, ThreadStatus::OP_FLUSH, 0);
|
||||
ASSERT_TRUE(VerifyOperationCount(env_, ThreadStatus::OP_FLUSH, 0));
|
||||
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
VerifyOperationCount(env_, ThreadStatus::OP_FLUSH, 0);
|
||||
ASSERT_TRUE(VerifyOperationCount(env_, ThreadStatus::OP_FLUSH, 0));
|
||||
|
||||
uint64_t num_running_flushes = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningFlushes,
|
||||
@@ -4808,7 +4814,7 @@ TEST_F(DBTest, ThreadStatusFlush) {
|
||||
// The first sync point is to make sure there's one flush job
|
||||
// running when we perform VerifyOperationCount().
|
||||
TEST_SYNC_POINT("DBTest::ThreadStatusFlush:1");
|
||||
VerifyOperationCount(env_, ThreadStatus::OP_FLUSH, 1);
|
||||
ASSERT_TRUE(VerifyOperationCount(env_, ThreadStatus::OP_FLUSH, 1));
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningFlushes,
|
||||
&num_running_flushes));
|
||||
ASSERT_EQ(num_running_flushes, 1);
|
||||
@@ -4819,17 +4825,11 @@ TEST_F(DBTest, ThreadStatusFlush) {
|
||||
}
|
||||
|
||||
TEST_P(DBTestWithParam, ThreadStatusSingleCompaction) {
|
||||
const int kTestKeySize = 16;
|
||||
const int kTestValueSize = 984;
|
||||
const int kEntrySize = kTestKeySize + kTestValueSize;
|
||||
const int kEntriesPerBuffer = 100;
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.write_buffer_size = kEntrySize * kEntriesPerBuffer;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.target_file_size_base = options.write_buffer_size;
|
||||
options.max_bytes_for_level_base = options.target_file_size_base * 2;
|
||||
options.max_bytes_for_level_multiplier = 2;
|
||||
options.compression = kNoCompression;
|
||||
options = CurrentOptions(options);
|
||||
options.env = env_;
|
||||
@@ -4864,7 +4864,7 @@ TEST_P(DBTestWithParam, ThreadStatusSingleCompaction) {
|
||||
&num_running_compactions));
|
||||
ASSERT_EQ(num_running_compactions, 0);
|
||||
TEST_SYNC_POINT("DBTest::ThreadStatusSingleCompaction:0");
|
||||
ASSERT_GE(NumTableFilesAtLevel(0),
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0),
|
||||
options.level0_file_num_compaction_trigger);
|
||||
|
||||
// This makes sure at least one compaction is running.
|
||||
@@ -4872,10 +4872,18 @@ TEST_P(DBTestWithParam, ThreadStatusSingleCompaction) {
|
||||
|
||||
if (options.enable_thread_tracking) {
|
||||
// expecting one single L0 to L1 compaction
|
||||
VerifyOperationCount(env_, ThreadStatus::OP_COMPACTION, 1);
|
||||
// This test is flaky and fails here.
|
||||
bool match = VerifyOperationCount(env_, ThreadStatus::OP_COMPACTION, 1);
|
||||
if (!match) {
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
fprintf(stderr, "running compaction: %" PRIu64 " lsm state: %s\n",
|
||||
num_running_compactions, FilesPerLevel().c_str());
|
||||
}
|
||||
ASSERT_TRUE(match);
|
||||
} else {
|
||||
// If thread tracking is not enabled, compaction count should be 0.
|
||||
VerifyOperationCount(env_, ThreadStatus::OP_COMPACTION, 0);
|
||||
ASSERT_TRUE(VerifyOperationCount(env_, ThreadStatus::OP_COMPACTION, 0));
|
||||
}
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
|
||||
+83
-22
@@ -666,11 +666,25 @@ TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
// try the 1st WAL created during open
|
||||
ASSERT_OK(Put("key0", "value"));
|
||||
ASSERT_NE(options.manual_wal_flush, dbfull()->WALBufferIsEmpty());
|
||||
|
||||
ASSERT_OK(db_->LockWAL());
|
||||
|
||||
ASSERT_TRUE(dbfull()->WALBufferIsEmpty());
|
||||
uint64_t wal_num = dbfull()->TEST_GetCurrentLogNumber();
|
||||
// Manual flush with wait=false should abruptly fail with TryAgain
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.wait = false;
|
||||
for (bool allow_write_stall : {true, false}) {
|
||||
flush_opts.allow_write_stall = allow_write_stall;
|
||||
ASSERT_TRUE(db_->Flush(flush_opts).IsTryAgain());
|
||||
}
|
||||
ASSERT_EQ(wal_num, dbfull()->TEST_GetCurrentLogNumber());
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL());
|
||||
// try the 2nd wal created during SwitchWAL
|
||||
|
||||
// try the 2nd wal created during SwitchWAL (not locked this time)
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
ASSERT_NE(wal_num, dbfull()->TEST_GetCurrentLogNumber());
|
||||
ASSERT_OK(Put("key1", "value"));
|
||||
ASSERT_NE(options.manual_wal_flush, dbfull()->WALBufferIsEmpty());
|
||||
ASSERT_OK(db_->LockWAL());
|
||||
@@ -709,21 +723,57 @@ TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
}
|
||||
|
||||
TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
// This is a micro-stress test of LockWAL and concurrency handling.
|
||||
// It is considered the most convenient way to balance functional
|
||||
// coverage and reproducibility (vs. the two extremes of (a) unit tests
|
||||
// tailored to specific interleavings and (b) db_stress)
|
||||
Options options = GetOptions();
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("k1", "val"));
|
||||
ASSERT_OK(Put("k1", "k1_orig"));
|
||||
ASSERT_OK(db_->LockWAL()); // 0 -> 1
|
||||
auto frozen_seqno = db_->GetLatestSequenceNumber();
|
||||
std::atomic<bool> t1_completed{false};
|
||||
port::Thread t1{[&]() {
|
||||
// Won't finish until WAL unlocked
|
||||
ASSERT_OK(Put("k1", "val2"));
|
||||
t1_completed = true;
|
||||
|
||||
std::string ingest_file = dbname_ + "/external.sst";
|
||||
{
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
ASSERT_OK(sst_file_writer.Open(ingest_file));
|
||||
ASSERT_OK(sst_file_writer.Put("k2", "k2_val"));
|
||||
ExternalSstFileInfo external_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&external_info));
|
||||
}
|
||||
AcqRelAtomic<bool> parallel_ingest_completed{false};
|
||||
port::Thread parallel_ingest{[&]() {
|
||||
IngestExternalFileOptions ingest_opts;
|
||||
ingest_opts.move_files = true; // faster than copy
|
||||
// Shouldn't finish until WAL unlocked
|
||||
ASSERT_OK(db_->IngestExternalFile({ingest_file}, ingest_opts));
|
||||
parallel_ingest_completed.Store(true);
|
||||
}};
|
||||
|
||||
AcqRelAtomic<bool> flush_completed{false};
|
||||
port::Thread parallel_flush{[&]() {
|
||||
FlushOptions flush_opts;
|
||||
// NB: Flush with wait=false case is tested above in LockWALInEffect
|
||||
flush_opts.wait = true;
|
||||
// allow_write_stall = true blocks in fewer cases
|
||||
flush_opts.allow_write_stall = true;
|
||||
// Shouldn't finish until WAL unlocked
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
flush_completed.Store(true);
|
||||
}};
|
||||
|
||||
AcqRelAtomic<bool> parallel_put_completed{false};
|
||||
port::Thread parallel_put{[&]() {
|
||||
// This can make certain failure scenarios more likely:
|
||||
// sleep(1);
|
||||
// Shouldn't finish until WAL unlocked
|
||||
ASSERT_OK(Put("k1", "k1_mod"));
|
||||
parallel_put_completed.Store(true);
|
||||
}};
|
||||
|
||||
ASSERT_OK(db_->LockWAL()); // 1 -> 2
|
||||
// Read-only ops are OK
|
||||
ASSERT_EQ(Get("k1"), "val");
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
{
|
||||
std::vector<LiveFileStorageInfo> files;
|
||||
LiveFilesStorageInfoOptions lf_opts;
|
||||
@@ -732,29 +782,35 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo({lf_opts}, &files));
|
||||
}
|
||||
|
||||
port::Thread t2{[&]() {
|
||||
port::Thread parallel_lock_wal{[&]() {
|
||||
ASSERT_OK(db_->LockWAL()); // 2 -> 3 or 1 -> 2
|
||||
}};
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1 or 3 -> 2
|
||||
// Give t1 an extra chance to jump in case of bug
|
||||
// Give parallel_put an extra chance to jump in case of bug
|
||||
std::this_thread::yield();
|
||||
t2.join();
|
||||
ASSERT_FALSE(t1_completed.load());
|
||||
parallel_lock_wal.join();
|
||||
ASSERT_FALSE(parallel_put_completed.Load());
|
||||
ASSERT_FALSE(parallel_ingest_completed.Load());
|
||||
ASSERT_FALSE(flush_completed.Load());
|
||||
|
||||
// Should now have 2 outstanding LockWAL
|
||||
ASSERT_EQ(Get("k1"), "val");
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1
|
||||
|
||||
ASSERT_FALSE(t1_completed.load());
|
||||
ASSERT_EQ(Get("k1"), "val");
|
||||
ASSERT_FALSE(parallel_put_completed.Load());
|
||||
ASSERT_FALSE(parallel_ingest_completed.Load());
|
||||
ASSERT_FALSE(flush_completed.Load());
|
||||
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
ASSERT_EQ(Get("k2"), "NOT_FOUND");
|
||||
ASSERT_EQ(frozen_seqno, db_->GetLatestSequenceNumber());
|
||||
|
||||
// Ensure final Unlock is concurrency safe and extra Unlock is safe but
|
||||
// non-OK
|
||||
std::atomic<int> unlock_ok{0};
|
||||
port::Thread t3{[&]() {
|
||||
port::Thread parallel_stuff{[&]() {
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
@@ -767,18 +823,23 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
t3.join();
|
||||
parallel_stuff.join();
|
||||
|
||||
// There was one extra unlock, so just one non-ok
|
||||
ASSERT_EQ(unlock_ok.load(), 2);
|
||||
|
||||
// Write can proceed
|
||||
t1.join();
|
||||
ASSERT_TRUE(t1_completed.load());
|
||||
ASSERT_EQ(Get("k1"), "val2");
|
||||
parallel_put.join();
|
||||
ASSERT_TRUE(parallel_put_completed.Load());
|
||||
ASSERT_EQ(Get("k1"), "k1_mod");
|
||||
parallel_ingest.join();
|
||||
ASSERT_TRUE(parallel_ingest_completed.Load());
|
||||
ASSERT_EQ(Get("k2"), "k2_val");
|
||||
parallel_flush.join();
|
||||
ASSERT_TRUE(flush_completed.Load());
|
||||
// And new writes
|
||||
ASSERT_OK(Put("k2", "val"));
|
||||
ASSERT_EQ(Get("k2"), "val");
|
||||
ASSERT_OK(Put("k3", "val"));
|
||||
ASSERT_EQ(Get("k3"), "val");
|
||||
}
|
||||
|
||||
TEST_P(DBWriteTest, ConcurrentlyDisabledWAL) {
|
||||
|
||||
+19
-4
@@ -47,6 +47,8 @@ EntryType GetEntryType(ValueType value_type) {
|
||||
return kEntryBlobIndex;
|
||||
case kTypeWideColumnEntity:
|
||||
return kEntryWideColumnEntity;
|
||||
case kTypeValuePreferredSeqno:
|
||||
return kEntryTimedPut;
|
||||
default:
|
||||
return kEntryOther;
|
||||
}
|
||||
@@ -155,10 +157,23 @@ void ReplaceInternalKeyWithMinTimestamp(std::string* result, const Slice& key,
|
||||
result->append(key.data() + key_sz - kNumInternalBytes, kNumInternalBytes);
|
||||
}
|
||||
|
||||
std::string ParsedInternalKey::DebugString(bool log_err_key, bool hex) const {
|
||||
std::string ParsedInternalKey::DebugString(bool log_err_key, bool hex,
|
||||
const Comparator* ucmp) const {
|
||||
std::string result = "'";
|
||||
size_t ts_sz_for_debug = ucmp == nullptr ? 0 : ucmp->timestamp_size();
|
||||
if (log_err_key) {
|
||||
result += user_key.ToString(hex);
|
||||
if (ts_sz_for_debug == 0) {
|
||||
result += user_key.ToString(hex);
|
||||
} else {
|
||||
assert(user_key.size() >= ts_sz_for_debug);
|
||||
Slice user_key_without_ts = user_key;
|
||||
user_key_without_ts.remove_suffix(ts_sz_for_debug);
|
||||
result += user_key_without_ts.ToString(hex);
|
||||
Slice ts = Slice(user_key.data() + user_key.size() - ts_sz_for_debug,
|
||||
ts_sz_for_debug);
|
||||
result += "|timestamp:";
|
||||
result += ucmp->TimestampToString(ts);
|
||||
}
|
||||
} else {
|
||||
result += "<redacted>";
|
||||
}
|
||||
@@ -171,11 +186,11 @@ std::string ParsedInternalKey::DebugString(bool log_err_key, bool hex) const {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string InternalKey::DebugString(bool hex) const {
|
||||
std::string InternalKey::DebugString(bool hex, const Comparator* ucmp) const {
|
||||
std::string result;
|
||||
ParsedInternalKey parsed;
|
||||
if (ParseInternalKey(rep_, &parsed, false /* log_err_key */).ok()) {
|
||||
result = parsed.DebugString(true /* log_err_key */, hex); // TODO
|
||||
result = parsed.DebugString(true /* log_err_key */, hex, ucmp); // TODO
|
||||
} else {
|
||||
result = "(bad)";
|
||||
result.append(EscapeString(rep_));
|
||||
|
||||
+3
-2
@@ -148,7 +148,8 @@ struct ParsedInternalKey {
|
||||
// u contains timestamp if user timestamp feature is enabled.
|
||||
ParsedInternalKey(const Slice& u, const SequenceNumber& seq, ValueType t)
|
||||
: user_key(u), sequence(seq), type(t) {}
|
||||
std::string DebugString(bool log_err_key, bool hex) const;
|
||||
std::string DebugString(bool log_err_key, bool hex,
|
||||
const Comparator* ucmp = nullptr) const;
|
||||
|
||||
void clear() {
|
||||
user_key.clear();
|
||||
@@ -503,7 +504,7 @@ class InternalKey {
|
||||
AppendInternalKeyFooter(&rep_, s, t);
|
||||
}
|
||||
|
||||
std::string DebugString(bool hex) const;
|
||||
std::string DebugString(bool hex, const Comparator* ucmp = nullptr) const;
|
||||
};
|
||||
|
||||
inline int InternalKeyComparator::Compare(const InternalKey& a,
|
||||
|
||||
@@ -30,6 +30,14 @@ class DBErrorHandlingFSTest : public DBTestBase {
|
||||
fault_env_.reset(new CompositeEnvWrapper(env_, fault_fs_));
|
||||
}
|
||||
|
||||
~DBErrorHandlingFSTest() {
|
||||
// Before destroying fault_env_
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->LoadDependency({});
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
std::string GetManifestNameFromLiveFiles() {
|
||||
std::vector<std::string> live_files;
|
||||
uint64_t manifest_size;
|
||||
|
||||
@@ -953,13 +953,15 @@ Status ExternalSstFileIngestionJob::AssignLevelAndSeqnoForIngestedFile(
|
||||
*assigned_seqno = 0;
|
||||
auto ucmp = cfd_->user_comparator();
|
||||
const size_t ts_sz = ucmp->timestamp_size();
|
||||
if (force_global_seqno || files_overlap_) {
|
||||
if (force_global_seqno || files_overlap_ ||
|
||||
compaction_style == kCompactionStyleFIFO) {
|
||||
*assigned_seqno = last_seqno + 1;
|
||||
// If files overlap, we have to ingest them at level 0.
|
||||
if (files_overlap_) {
|
||||
if (files_overlap_ || compaction_style == kCompactionStyleFIFO) {
|
||||
assert(ts_sz == 0);
|
||||
file_to_ingest->picked_level = 0;
|
||||
if (ingestion_options_.fail_if_not_bottommost_level) {
|
||||
if (ingestion_options_.fail_if_not_bottommost_level &&
|
||||
cfd_->NumberLevels() > 1) {
|
||||
status = Status::TryAgain(
|
||||
"Files cannot be ingested to Lmax. Please make sure key range of "
|
||||
"Lmax does not overlap with files to ingest.");
|
||||
|
||||
@@ -3082,6 +3082,68 @@ TEST_P(ExternalSSTFileTest,
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, FIFOCompaction) {
|
||||
// FIFO always ingests SST files to L0 and assign latest sequence number.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 1;
|
||||
options.compaction_style = kCompactionStyleFIFO;
|
||||
options.max_open_files = -1;
|
||||
DestroyAndReopen(options);
|
||||
std::map<std::string, std::string> true_data;
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
ASSERT_OK(Put(Key(i), Key(i) + "_val"));
|
||||
true_data[Key(i)] = Key(i) + "_val";
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ("1", FilesPerLevel());
|
||||
std::vector<std::pair<std::string, std::string>> file_data;
|
||||
for (int i = 0; i <= 20; i++) {
|
||||
file_data.emplace_back(Key(i), Key(i) + "_ingest");
|
||||
}
|
||||
// Overlaps with memtable, will trigger flush
|
||||
ASSERT_OK(GenerateAndAddExternalFile(options, file_data, -1,
|
||||
/*allow_global_seqno=*/true, true, false,
|
||||
false, false, &true_data));
|
||||
ASSERT_EQ("2", FilesPerLevel());
|
||||
|
||||
file_data.clear();
|
||||
for (int i = 100; i <= 120; i++) {
|
||||
file_data.emplace_back(Key(i), Key(i) + "_ingest");
|
||||
}
|
||||
// global sequence number is always assigned, so this will fail
|
||||
ASSERT_NOK(GenerateAndAddExternalFile(options, file_data, -1,
|
||||
/*allow_global_seqno=*/false, true,
|
||||
false, false, false, &true_data));
|
||||
ASSERT_OK(GenerateAndAddExternalFile(options, file_data, -1,
|
||||
/*allow_global_seqno=*/true, true, false,
|
||||
false, false, &true_data));
|
||||
|
||||
// Compact to data to lower level to test multi-level FIFO later
|
||||
options.num_levels = 7;
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
ASSERT_OK(TryReopen(options));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
ASSERT_EQ("0,0,0,0,0,0,1", FilesPerLevel());
|
||||
|
||||
options.num_levels = 7;
|
||||
options.compaction_style = kCompactionStyleFIFO;
|
||||
ASSERT_OK(TryReopen(options));
|
||||
file_data.clear();
|
||||
for (int i = 200; i <= 220; i++) {
|
||||
file_data.emplace_back(Key(i), Key(i) + "_ingest");
|
||||
}
|
||||
// Files are ingested into L0 for multi-level FIFO
|
||||
ASSERT_OK(GenerateAndAddExternalFile(options, file_data, -1,
|
||||
/*allow_global_seqno=*/true, true, false,
|
||||
false, false, &true_data));
|
||||
|
||||
ASSERT_EQ("1,0,0,0,0,0,1", FilesPerLevel());
|
||||
VerifyDBFromMap(true_data);
|
||||
}
|
||||
|
||||
class ExternalSSTFileWithTimestampTest : public ExternalSSTFileTest {
|
||||
public:
|
||||
ExternalSSTFileWithTimestampTest() = default;
|
||||
|
||||
@@ -175,22 +175,29 @@ Status ImportColumnFamilyJob::Run() {
|
||||
static_cast<uint64_t>(temp_current_time);
|
||||
}
|
||||
|
||||
// Recover files' epoch number using dummy VersionStorageInfo
|
||||
VersionBuilder dummy_version_builder(
|
||||
cfd_->current()->version_set()->file_options(), cfd_->ioptions(),
|
||||
cfd_->table_cache(), cfd_->current()->storage_info(),
|
||||
cfd_->current()->version_set(),
|
||||
cfd_->GetFileMetadataCacheReservationManager());
|
||||
VersionStorageInfo dummy_vstorage(
|
||||
&cfd_->internal_comparator(), cfd_->user_comparator(),
|
||||
cfd_->NumberLevels(), cfd_->ioptions()->compaction_style,
|
||||
nullptr /* src_vstorage */, cfd_->ioptions()->force_consistency_checks,
|
||||
EpochNumberRequirement::kMightMissing, cfd_->ioptions()->clock,
|
||||
cfd_->GetLatestMutableCFOptions()->bottommost_file_compaction_delay,
|
||||
cfd_->current()->version_set()->offpeak_time_option());
|
||||
Status s;
|
||||
|
||||
// When importing multiple CFs, we should not reuse epoch number from ingested
|
||||
// files. Since these epoch numbers were assigned by different CFs, there may
|
||||
// be different files from different CFs with the same epoch number. With a
|
||||
// subsequent intra-L0 compaction we may end up with files with overlapping
|
||||
// key range but the same epoch number. Here we will create a dummy
|
||||
// VersionStorageInfo per CF being imported. Each CF's files will be assigned
|
||||
// increasing epoch numbers to avoid duplicated epoch number. This is done by
|
||||
// only resetting epoch number of the new CF in the first call to
|
||||
// RecoverEpochNumbers() below.
|
||||
for (size_t i = 0; s.ok() && i < files_to_import_.size(); ++i) {
|
||||
VersionBuilder dummy_version_builder(
|
||||
cfd_->current()->version_set()->file_options(), cfd_->ioptions(),
|
||||
cfd_->table_cache(), cfd_->current()->storage_info(),
|
||||
cfd_->current()->version_set(),
|
||||
cfd_->GetFileMetadataCacheReservationManager());
|
||||
VersionStorageInfo dummy_vstorage(
|
||||
&cfd_->internal_comparator(), cfd_->user_comparator(),
|
||||
cfd_->NumberLevels(), cfd_->ioptions()->compaction_style,
|
||||
nullptr /* src_vstorage */, cfd_->ioptions()->force_consistency_checks,
|
||||
EpochNumberRequirement::kMightMissing, cfd_->ioptions()->clock,
|
||||
cfd_->GetLatestMutableCFOptions()->bottommost_file_compaction_delay,
|
||||
cfd_->current()->version_set()->offpeak_time_option());
|
||||
for (size_t j = 0; s.ok() && j < files_to_import_[i].size(); ++j) {
|
||||
const auto& f = files_to_import_[i][j];
|
||||
const auto& file_metadata = *metadatas_[i][j];
|
||||
@@ -218,42 +225,39 @@ Status ImportColumnFamilyJob::Run() {
|
||||
f.table_properties.user_defined_timestamps_persisted));
|
||||
s = dummy_version_builder.Apply(&dummy_version_edit);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
s = dummy_version_builder.SaveTo(&dummy_vstorage);
|
||||
}
|
||||
if (s.ok()) {
|
||||
dummy_vstorage.RecoverEpochNumbers(cfd_);
|
||||
}
|
||||
|
||||
// Record changes from this CF import in VersionEdit, including files with
|
||||
// recovered epoch numbers
|
||||
if (s.ok()) {
|
||||
edit_.SetColumnFamily(cfd_->GetID());
|
||||
if (s.ok()) {
|
||||
s = dummy_version_builder.SaveTo(&dummy_vstorage);
|
||||
}
|
||||
if (s.ok()) {
|
||||
// force resetting epoch number for each file
|
||||
dummy_vstorage.RecoverEpochNumbers(cfd_, /*restart_epoch=*/i == 0,
|
||||
/*force=*/true);
|
||||
edit_.SetColumnFamily(cfd_->GetID());
|
||||
|
||||
for (int level = 0; level < dummy_vstorage.num_levels(); level++) {
|
||||
for (FileMetaData* file_meta : dummy_vstorage.LevelFiles(level)) {
|
||||
edit_.AddFile(level, *file_meta);
|
||||
// If incoming sequence number is higher, update local sequence
|
||||
// number.
|
||||
if (file_meta->fd.largest_seqno > versions_->LastSequence()) {
|
||||
versions_->SetLastAllocatedSequence(file_meta->fd.largest_seqno);
|
||||
versions_->SetLastPublishedSequence(file_meta->fd.largest_seqno);
|
||||
versions_->SetLastSequence(file_meta->fd.largest_seqno);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Release resources occupied by the dummy VersionStorageInfo
|
||||
for (int level = 0; level < dummy_vstorage.num_levels(); level++) {
|
||||
for (FileMetaData* file_meta : dummy_vstorage.LevelFiles(level)) {
|
||||
edit_.AddFile(level, *file_meta);
|
||||
// If incoming sequence number is higher, update local sequence number.
|
||||
if (file_meta->fd.largest_seqno > versions_->LastSequence()) {
|
||||
versions_->SetLastAllocatedSequence(file_meta->fd.largest_seqno);
|
||||
versions_->SetLastPublishedSequence(file_meta->fd.largest_seqno);
|
||||
versions_->SetLastSequence(file_meta->fd.largest_seqno);
|
||||
file_meta->refs--;
|
||||
if (file_meta->refs <= 0) {
|
||||
delete file_meta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release resources occupied by the dummy VersionStorageInfo
|
||||
for (int level = 0; level < dummy_vstorage.num_levels(); level++) {
|
||||
for (FileMetaData* file_meta : dummy_vstorage.LevelFiles(level)) {
|
||||
file_meta->refs--;
|
||||
if (file_meta->refs <= 0) {
|
||||
delete file_meta;
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -946,6 +946,108 @@ TEST_F(ImportColumnFamilyTest, ImportMultiColumnFamilySeveralFilesWithOverlap) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(second_cfh));
|
||||
}
|
||||
|
||||
TEST_F(ImportColumnFamilyTest, AssignEpochNumberToMultipleCF) {
|
||||
// Test ingesting CFs where L0 files could have the same epoch number.
|
||||
Options options = CurrentOptions();
|
||||
options.level_compaction_dynamic_level_bytes = true;
|
||||
options.max_background_jobs = 8;
|
||||
env_->SetBackgroundThreads(2, Env::LOW);
|
||||
env_->SetBackgroundThreads(0, Env::BOTTOM);
|
||||
CreateAndReopenWithCF({"CF1", "CF2"}, options);
|
||||
|
||||
// CF1:
|
||||
// L6: [0, 99], [100, 199]
|
||||
// CF2:
|
||||
// L6: [1000, 1099], [1100, 1199]
|
||||
for (int i = 100; i < 200; ++i) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i) + "_val"));
|
||||
ASSERT_OK(Put(2, Key(1000 + i), Key(1000 + i) + "_val"));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i) + "_val"));
|
||||
ASSERT_OK(Put(2, Key(1000 + i), Key(1000 + i) + "_val"));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
MoveFilesToLevel(6, 1);
|
||||
MoveFilesToLevel(6, 2);
|
||||
|
||||
// CF1:
|
||||
// level 0 epoch: 5 file num 30 smallest key000010 - key000019
|
||||
// level 0 epoch: 4 file num 27 smallest key000000 - key000009
|
||||
// level 0 epoch: 3 file num 23 smallest key000100 - key000199
|
||||
// level 6 epoch: 2 file num 20 smallest key000000 - key000099
|
||||
// level 6 epoch: 1 file num 17 smallest key000100 - key000199
|
||||
// CF2:
|
||||
// level 0 epoch: 5 file num 31 smallest key001010 - key001019
|
||||
// level 0 epoch: 4 file num 28 smallest key001000 - key001009
|
||||
// level 0 epoch: 3 file num 25 smallest key001020 - key001029
|
||||
// level 6 epoch: 2 file num 21 smallest key001000 - key001099
|
||||
// level 6 epoch: 1 file num 18 smallest key001100 - key001199
|
||||
for (int i = 100; i < 200; ++i) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i) + "_val"));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
for (int i = 20; i < 30; ++i) {
|
||||
ASSERT_OK(Put(2, Key(i + 1000), Key(i + 1000) + "_val"));
|
||||
}
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i) + "_val"));
|
||||
ASSERT_OK(Put(2, Key(i + 1000), Key(i + 1000) + "_val"));
|
||||
if (i % 10 == 9) {
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
}
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
// Create a CF by importing these two CF1 and CF2.
|
||||
// Then two compactions will be triggerred, one to compact from L0
|
||||
// to L6 (files #23 and #17), and another to do intra-L0 compaction
|
||||
// for the rest of the L0 files. Before a bug fix, we used to
|
||||
// directly use the epoch numbers from the ingested files in the new CF.
|
||||
// This means different files from different CFs can have the same epoch
|
||||
// number. If the intra-L0 compaction finishes first, it can cause a
|
||||
// corruption where two L0 files can have the same epoch number but
|
||||
// with overlapping key range.
|
||||
Checkpoint* checkpoint1;
|
||||
ASSERT_OK(Checkpoint::Create(db_, &checkpoint1));
|
||||
ASSERT_OK(checkpoint1->ExportColumnFamily(handles_[1], export_files_dir_,
|
||||
&metadata_ptr_));
|
||||
ASSERT_OK(checkpoint1->ExportColumnFamily(handles_[2], export_files_dir2_,
|
||||
&metadata_ptr2_));
|
||||
ASSERT_NE(metadata_ptr_, nullptr);
|
||||
ASSERT_NE(metadata_ptr2_, nullptr);
|
||||
|
||||
std::atomic_int compaction_counter = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:BeforeRun",
|
||||
[&compaction_counter](void*) {
|
||||
compaction_counter++;
|
||||
if (compaction_counter == 1) {
|
||||
// Wait for the next compaction to finish
|
||||
TEST_SYNC_POINT("WaitForSecondCompaction");
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::BackgroundCompaction:AfterCompaction",
|
||||
"WaitForSecondCompaction"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ImportColumnFamilyOptions import_options;
|
||||
import_options.move_files = false;
|
||||
std::vector<const ExportImportFilesMetaData*> metadatas = {metadata_ptr_,
|
||||
metadata_ptr2_};
|
||||
ASSERT_OK(db_->CreateColumnFamilyWithImport(options, "CF3", import_options,
|
||||
metadatas, &import_cfh_));
|
||||
WaitForCompactOptions o;
|
||||
ASSERT_OK(db_->WaitForCompact(o));
|
||||
delete checkpoint1;
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+9
-4
@@ -487,9 +487,11 @@ unsigned int Reader::ReadPhysicalRecord(Slice* result, size_t* drop_size,
|
||||
type == kRecyclableUserDefinedTimestampSizeType);
|
||||
if (is_recyclable_type) {
|
||||
header_size = kRecyclableHeaderSize;
|
||||
if (end_of_buffer_offset_ - buffer_.size() == 0) {
|
||||
recycled_ = true;
|
||||
if (first_record_read_ && !recycled_) {
|
||||
// A recycled log should have started with a recycled record
|
||||
return kBadRecord;
|
||||
}
|
||||
recycled_ = true;
|
||||
// We need enough for the larger header
|
||||
if (buffer_.size() < static_cast<size_t>(kRecyclableHeaderSize)) {
|
||||
int r = kEof;
|
||||
@@ -867,9 +869,12 @@ bool FragmentBufferedReader::TryReadFragment(
|
||||
int header_size = kHeaderSize;
|
||||
if ((type >= kRecyclableFullType && type <= kRecyclableLastType) ||
|
||||
type == kRecyclableUserDefinedTimestampSizeType) {
|
||||
if (end_of_buffer_offset_ - buffer_.size() == 0) {
|
||||
recycled_ = true;
|
||||
if (first_record_read_ && !recycled_) {
|
||||
// A recycled log should have started with a recycled record
|
||||
*fragment_type_or_err = kBadRecord;
|
||||
return true;
|
||||
}
|
||||
recycled_ = true;
|
||||
header_size = kRecyclableHeaderSize;
|
||||
while (buffer_.size() < static_cast<size_t>(kRecyclableHeaderSize)) {
|
||||
size_t old_size = buffer_.size();
|
||||
|
||||
@@ -774,6 +774,32 @@ TEST_P(LogTest, RecycleWithTimestampSize) {
|
||||
ASSERT_EQ("EOF", Read());
|
||||
}
|
||||
|
||||
// Validates that `MaybeAddUserDefinedTimestampSizeRecord`` adds padding to the
|
||||
// tail of a block and switches to a new block, if there's not enough space for
|
||||
// the record.
|
||||
TEST_P(LogTest, TimestampSizeRecordPadding) {
|
||||
bool recyclable_log = (std::get<0>(GetParam()) != 0);
|
||||
const size_t header_size =
|
||||
recyclable_log ? kRecyclableHeaderSize : kHeaderSize;
|
||||
const size_t data_len = kBlockSize - 2 * header_size;
|
||||
|
||||
const auto first_str = BigString("foo", data_len);
|
||||
Write(first_str);
|
||||
|
||||
UnorderedMap<uint32_t, size_t> ts_sz = {
|
||||
{2, sizeof(uint64_t)},
|
||||
};
|
||||
ASSERT_OK(
|
||||
writer_->MaybeAddUserDefinedTimestampSizeRecord(WriteOptions(), ts_sz));
|
||||
ASSERT_LT(writer_->TEST_block_offset(), kBlockSize);
|
||||
|
||||
const auto second_str = BigString("bar", 1000);
|
||||
Write(second_str);
|
||||
|
||||
ASSERT_EQ(first_str, Read());
|
||||
CheckRecordAndTimestampSize(second_str, ts_sz);
|
||||
}
|
||||
|
||||
// Do NOT enable compression for this instantiation.
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
Log, LogTest,
|
||||
@@ -1131,6 +1157,42 @@ TEST_P(CompressionLogTest, AlignedFragmentation) {
|
||||
ASSERT_EQ("EOF", Read());
|
||||
}
|
||||
|
||||
TEST_P(CompressionLogTest, ChecksumMismatch) {
|
||||
const CompressionType kCompressionType = std::get<2>(GetParam());
|
||||
const bool kCompressionEnabled = kCompressionType != kNoCompression;
|
||||
const bool kRecyclableLog = (std::get<0>(GetParam()) != 0);
|
||||
if (!StreamingCompressionTypeSupported(kCompressionType)) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires support for compression type");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(SetupTestEnv());
|
||||
|
||||
Write("foooooo");
|
||||
int header_len;
|
||||
if (kRecyclableLog) {
|
||||
header_len = kRecyclableHeaderSize;
|
||||
} else {
|
||||
header_len = kHeaderSize;
|
||||
}
|
||||
int compression_record_len;
|
||||
if (kCompressionEnabled) {
|
||||
compression_record_len = header_len + 4;
|
||||
} else {
|
||||
compression_record_len = 0;
|
||||
}
|
||||
IncrementByte(compression_record_len + header_len /* offset */,
|
||||
14 /* delta */);
|
||||
|
||||
ASSERT_EQ("EOF", Read());
|
||||
if (!kRecyclableLog) {
|
||||
ASSERT_GT(DroppedBytes(), 0U);
|
||||
ASSERT_EQ("OK", MatchError("checksum mismatch"));
|
||||
} else {
|
||||
ASSERT_EQ(0U, DroppedBytes());
|
||||
ASSERT_EQ("", ReportMessage());
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
Compression, CompressionLogTest,
|
||||
::testing::Combine(::testing::Values(0, 1), ::testing::Bool(),
|
||||
|
||||
+26
-10
@@ -27,6 +27,8 @@ Writer::Writer(std::unique_ptr<WritableFileWriter>&& dest, uint64_t log_number,
|
||||
block_offset_(0),
|
||||
log_number_(log_number),
|
||||
recycle_log_files_(recycle_log_files),
|
||||
// Header size varies depending on whether we are recycling or not.
|
||||
header_size_(recycle_log_files ? kRecyclableHeaderSize : kHeaderSize),
|
||||
manual_flush_(manual_flush),
|
||||
compression_type_(compression_type),
|
||||
compress_(nullptr) {
|
||||
@@ -80,10 +82,6 @@ IOStatus Writer::AddRecord(const WriteOptions& write_options,
|
||||
const char* ptr = slice.data();
|
||||
size_t left = slice.size();
|
||||
|
||||
// Header size varies depending on whether we are recycling or not.
|
||||
const int header_size =
|
||||
recycle_log_files_ ? kRecyclableHeaderSize : kHeaderSize;
|
||||
|
||||
// Fragment the record if necessary and emit it. Note that if slice
|
||||
// is empty, we still want to iterate once to emit a single
|
||||
// zero-length record
|
||||
@@ -102,12 +100,12 @@ IOStatus Writer::AddRecord(const WriteOptions& write_options,
|
||||
do {
|
||||
const int64_t leftover = kBlockSize - block_offset_;
|
||||
assert(leftover >= 0);
|
||||
if (leftover < header_size) {
|
||||
if (leftover < header_size_) {
|
||||
// Switch to a new block
|
||||
if (leftover > 0) {
|
||||
// Fill the trailer (literal below relies on kHeaderSize and
|
||||
// kRecyclableHeaderSize being <= 11)
|
||||
assert(header_size <= 11);
|
||||
assert(header_size_ <= 11);
|
||||
s = dest_->Append(opts,
|
||||
Slice("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
static_cast<size_t>(leftover)),
|
||||
@@ -120,9 +118,9 @@ IOStatus Writer::AddRecord(const WriteOptions& write_options,
|
||||
}
|
||||
|
||||
// Invariant: we never leave < header_size bytes in a block.
|
||||
assert(static_cast<int64_t>(kBlockSize - block_offset_) >= header_size);
|
||||
assert(static_cast<int64_t>(kBlockSize - block_offset_) >= header_size_);
|
||||
|
||||
const size_t avail = kBlockSize - block_offset_ - header_size;
|
||||
const size_t avail = kBlockSize - block_offset_ - header_size_;
|
||||
|
||||
// Compress the record if compression is enabled.
|
||||
// Compress() is called at least once (compress_start=true) and after the
|
||||
@@ -203,8 +201,7 @@ IOStatus Writer::AddCompressionTypeRecord(const WriteOptions& write_options) {
|
||||
}
|
||||
}
|
||||
// Initialize fields required for compression
|
||||
const size_t max_output_buffer_len =
|
||||
kBlockSize - (recycle_log_files_ ? kRecyclableHeaderSize : kHeaderSize);
|
||||
const size_t max_output_buffer_len = kBlockSize - header_size_;
|
||||
CompressionOptions opts;
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
compress_ = StreamingCompress::Create(compression_type_, opts,
|
||||
@@ -244,6 +241,25 @@ IOStatus Writer::MaybeAddUserDefinedTimestampSizeRecord(
|
||||
record.EncodeTo(&encoded);
|
||||
RecordType type = recycle_log_files_ ? kRecyclableUserDefinedTimestampSizeType
|
||||
: kUserDefinedTimestampSizeType;
|
||||
|
||||
// If there's not enough space for this record, switch to a new block.
|
||||
const int64_t leftover = kBlockSize - block_offset_;
|
||||
if (leftover < header_size_ + (int)encoded.size()) {
|
||||
IOOptions opts;
|
||||
IOStatus s = WritableFileWriter::PrepareIOOptions(write_options, opts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<char> trailer(leftover, '\x00');
|
||||
s = dest_->Append(opts, Slice(trailer.data(), trailer.size()));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
block_offset_ = 0;
|
||||
}
|
||||
|
||||
return EmitPhysicalRecord(write_options, type, encoded.data(),
|
||||
encoded.size());
|
||||
}
|
||||
|
||||
@@ -109,11 +109,14 @@ class Writer {
|
||||
|
||||
bool BufferIsEmpty();
|
||||
|
||||
size_t TEST_block_offset() const { return block_offset_; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<WritableFileWriter> dest_;
|
||||
size_t block_offset_; // Current offset in block
|
||||
uint64_t log_number_;
|
||||
bool recycle_log_files_;
|
||||
int header_size_;
|
||||
|
||||
// crc32c values for all supported record types. These are
|
||||
// pre-computed to reduce the overhead of computing the crc of the
|
||||
|
||||
+17
-36
@@ -928,21 +928,10 @@ struct Saver {
|
||||
} // anonymous namespace
|
||||
|
||||
static bool SaveValue(void* arg, const char* entry) {
|
||||
TEST_SYNC_POINT_CALLBACK("Memtable::SaveValue:Begin:entry", &entry);
|
||||
Saver* s = static_cast<Saver*>(arg);
|
||||
assert(s != nullptr);
|
||||
assert(!s->value || !s->columns);
|
||||
|
||||
if (s->protection_bytes_per_key > 0) {
|
||||
*(s->status) = MemTable::VerifyEntryChecksum(
|
||||
entry, s->protection_bytes_per_key, s->allow_data_in_errors);
|
||||
if (!s->status->ok()) {
|
||||
ROCKS_LOG_ERROR(s->logger, "In SaveValue: %s", s->status->getState());
|
||||
// Memtable entry corrupted
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MergeContext* merge_context = s->merge_context;
|
||||
SequenceNumber max_covering_tombstone_seq = s->max_covering_tombstone_seq;
|
||||
const MergeOperator* merge_operator = s->merge_operator;
|
||||
@@ -965,6 +954,22 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
if (user_comparator->EqualWithoutTimestamp(user_key_slice,
|
||||
s->key->user_key())) {
|
||||
// Correct user key
|
||||
TEST_SYNC_POINT_CALLBACK("Memtable::SaveValue:Found:entry", &entry);
|
||||
std::unique_ptr<ReadLock> read_lock;
|
||||
if (s->inplace_update_support) {
|
||||
read_lock.reset(new ReadLock(s->mem->GetLock(s->key->user_key())));
|
||||
}
|
||||
|
||||
if (s->protection_bytes_per_key > 0) {
|
||||
*(s->status) = MemTable::VerifyEntryChecksum(
|
||||
entry, s->protection_bytes_per_key, s->allow_data_in_errors);
|
||||
if (!s->status->ok()) {
|
||||
ROCKS_LOG_ERROR(s->logger, "In SaveValue: %s", s->status->getState());
|
||||
// Memtable entry corrupted
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
|
||||
ValueType type;
|
||||
SequenceNumber seq;
|
||||
@@ -1035,10 +1040,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s->inplace_update_support) {
|
||||
s->mem->GetLock(s->key->user_key())->ReadLock();
|
||||
}
|
||||
|
||||
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
|
||||
|
||||
*(s->status) = Status::OK();
|
||||
@@ -1049,10 +1050,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
s->columns->SetPlainValue(v);
|
||||
}
|
||||
|
||||
if (s->inplace_update_support) {
|
||||
s->mem->GetLock(s->key->user_key())->ReadUnlock();
|
||||
}
|
||||
|
||||
*(s->found_final_value) = true;
|
||||
*(s->is_blob_index) = true;
|
||||
|
||||
@@ -1060,10 +1057,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
}
|
||||
case kTypeValue:
|
||||
case kTypeValuePreferredSeqno: {
|
||||
if (s->inplace_update_support) {
|
||||
s->mem->GetLock(s->key->user_key())->ReadLock();
|
||||
}
|
||||
|
||||
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
|
||||
|
||||
if (type == kTypeValuePreferredSeqno) {
|
||||
@@ -1100,10 +1093,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
s->columns->SetPlainValue(v);
|
||||
}
|
||||
|
||||
if (s->inplace_update_support) {
|
||||
s->mem->GetLock(s->key->user_key())->ReadUnlock();
|
||||
}
|
||||
|
||||
*(s->found_final_value) = true;
|
||||
|
||||
if (s->is_blob_index != nullptr) {
|
||||
@@ -1113,10 +1102,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
return false;
|
||||
}
|
||||
case kTypeWideColumnEntity: {
|
||||
if (s->inplace_update_support) {
|
||||
s->mem->GetLock(s->key->user_key())->ReadLock();
|
||||
}
|
||||
|
||||
Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
|
||||
|
||||
*(s->status) = Status::OK();
|
||||
@@ -1158,10 +1143,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->status) = s->columns->SetWideColumnValue(v);
|
||||
}
|
||||
|
||||
if (s->inplace_update_support) {
|
||||
s->mem->GetLock(s->key->user_key())->ReadUnlock();
|
||||
}
|
||||
|
||||
*(s->found_final_value) = true;
|
||||
|
||||
if (s->is_blob_index != nullptr) {
|
||||
@@ -1499,9 +1480,9 @@ Status MemTable::Update(SequenceNumber seq, ValueType value_type,
|
||||
|
||||
// Update value, if new value size <= previous value size
|
||||
if (new_size <= prev_size) {
|
||||
WriteLock wl(GetLock(lkey.user_key()));
|
||||
char* p =
|
||||
EncodeVarint32(const_cast<char*>(key_ptr) + key_length, new_size);
|
||||
WriteLock wl(GetLock(lkey.user_key()));
|
||||
memcpy(p, value.data(), value.size());
|
||||
assert((unsigned)((p + value.size()) - entry) ==
|
||||
(unsigned)(VarintLength(key_length) + key_length +
|
||||
|
||||
@@ -152,7 +152,7 @@ Status FileChecksumRetriever::ApplyVersionEdit(VersionEdit& edit,
|
||||
|
||||
VersionEditHandler::VersionEditHandler(
|
||||
bool read_only, std::vector<ColumnFamilyDescriptor> column_families,
|
||||
VersionSet* version_set, bool track_missing_files,
|
||||
VersionSet* version_set, bool track_found_and_missing_files,
|
||||
bool no_error_if_files_missing, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const ReadOptions& read_options, bool skip_load_table_files,
|
||||
EpochNumberRequirement epoch_number_requirement)
|
||||
@@ -160,7 +160,7 @@ VersionEditHandler::VersionEditHandler(
|
||||
read_only_(read_only),
|
||||
column_families_(std::move(column_families)),
|
||||
version_set_(version_set),
|
||||
track_missing_files_(track_missing_files),
|
||||
track_found_and_missing_files_(track_found_and_missing_files),
|
||||
no_error_if_files_missing_(no_error_if_files_missing),
|
||||
io_tracer_(io_tracer),
|
||||
skip_load_table_files_(skip_load_table_files),
|
||||
@@ -500,7 +500,8 @@ ColumnFamilyData* VersionEditHandler::CreateCfAndInit(
|
||||
assert(builders_.find(cf_id) == builders_.end());
|
||||
builders_.emplace(cf_id,
|
||||
VersionBuilderUPtr(new BaseReferencedVersionBuilder(cfd)));
|
||||
if (track_missing_files_) {
|
||||
if (track_found_and_missing_files_) {
|
||||
cf_to_found_files_.emplace(cf_id, std::unordered_set<uint64_t>());
|
||||
cf_to_missing_files_.emplace(cf_id, std::unordered_set<uint64_t>());
|
||||
cf_to_missing_blob_files_high_.emplace(cf_id, kInvalidBlobFileNumber);
|
||||
}
|
||||
@@ -513,7 +514,11 @@ ColumnFamilyData* VersionEditHandler::DestroyCfAndCleanup(
|
||||
auto builder_iter = builders_.find(cf_id);
|
||||
assert(builder_iter != builders_.end());
|
||||
builders_.erase(builder_iter);
|
||||
if (track_missing_files_) {
|
||||
if (track_found_and_missing_files_) {
|
||||
auto found_files_iter = cf_to_found_files_.find(cf_id);
|
||||
assert(found_files_iter != cf_to_found_files_.end());
|
||||
cf_to_found_files_.erase(found_files_iter);
|
||||
|
||||
auto missing_files_iter = cf_to_missing_files_.find(cf_id);
|
||||
assert(missing_files_iter != cf_to_missing_files_.end());
|
||||
cf_to_missing_files_.erase(missing_files_iter);
|
||||
@@ -729,7 +734,7 @@ VersionEditHandlerPointInTime::VersionEditHandlerPointInTime(
|
||||
const ReadOptions& read_options,
|
||||
EpochNumberRequirement epoch_number_requirement)
|
||||
: VersionEditHandler(read_only, column_families, version_set,
|
||||
/*track_missing_files=*/true,
|
||||
/*track_found_and_missing_files=*/true,
|
||||
/*no_error_if_files_missing=*/true, io_tracer,
|
||||
read_options, epoch_number_requirement) {}
|
||||
|
||||
@@ -824,6 +829,12 @@ void VersionEditHandlerPointInTime::CheckIterationResult(
|
||||
|
||||
version_set_->AppendVersion(cfd, v_iter->second);
|
||||
versions_.erase(v_iter);
|
||||
// Let's clear found_files, since any files in that are part of the
|
||||
// installed Version. Any files that got obsoleted would have already
|
||||
// been moved to intermediate_files_
|
||||
auto found_files_iter = cf_to_found_files_.find(cfd->GetID());
|
||||
assert(found_files_iter != cf_to_found_files_.end());
|
||||
found_files_iter->second.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -854,10 +865,16 @@ ColumnFamilyData* VersionEditHandlerPointInTime::DestroyCfAndCleanup(
|
||||
|
||||
Status VersionEditHandlerPointInTime::MaybeCreateVersion(
|
||||
const VersionEdit& edit, ColumnFamilyData* cfd, bool force_create_version) {
|
||||
TEST_SYNC_POINT("VersionEditHandlerPointInTime::MaybeCreateVersion:Begin1");
|
||||
TEST_SYNC_POINT("VersionEditHandlerPointInTime::MaybeCreateVersion:Begin2");
|
||||
assert(cfd != nullptr);
|
||||
if (!force_create_version) {
|
||||
assert(edit.GetColumnFamily() == cfd->GetID());
|
||||
}
|
||||
auto found_files_iter = cf_to_found_files_.find(cfd->GetID());
|
||||
assert(found_files_iter != cf_to_found_files_.end());
|
||||
std::unordered_set<uint64_t>& found_files = found_files_iter->second;
|
||||
|
||||
auto missing_files_iter = cf_to_missing_files_.find(cfd->GetID());
|
||||
assert(missing_files_iter != cf_to_missing_files_.end());
|
||||
std::unordered_set<uint64_t>& missing_files = missing_files_iter->second;
|
||||
@@ -889,6 +906,18 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersion(
|
||||
auto fiter = missing_files.find(file_num);
|
||||
if (fiter != missing_files.end()) {
|
||||
missing_files.erase(fiter);
|
||||
} else {
|
||||
fiter = found_files.find(file_num);
|
||||
// Only mark new files added during this catchup attempt for deletion.
|
||||
// These files were never installed in VersionStorageInfo.
|
||||
// Already referenced files that are deleted by a VersionEdit will
|
||||
// be added to the VersionStorageInfo's obsolete files when the old
|
||||
// version is dereferenced.
|
||||
if (fiter != found_files.end()) {
|
||||
intermediate_files_.emplace_back(
|
||||
MakeTableFileName(cfd->ioptions()->cf_paths[0].path, file_num));
|
||||
found_files.erase(fiter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,9 +933,14 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersion(
|
||||
s = VerifyFile(cfd, fpath, level, meta);
|
||||
if (s.IsPathNotFound() || s.IsNotFound() || s.IsCorruption()) {
|
||||
missing_files.insert(file_num);
|
||||
if (s.IsCorruption()) {
|
||||
found_files.insert(file_num);
|
||||
}
|
||||
s = Status::OK();
|
||||
} else if (!s.ok()) {
|
||||
break;
|
||||
} else {
|
||||
found_files.insert(file_num);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ using VersionBuilderUPtr = std::unique_ptr<BaseReferencedVersionBuilder>;
|
||||
// To use this class and its subclasses,
|
||||
// 1. Create an object of VersionEditHandler or its subclasses.
|
||||
// VersionEditHandler handler(read_only, column_families, version_set,
|
||||
// track_missing_files,
|
||||
// track_found_and_missing_files,
|
||||
// no_error_if_files_missing);
|
||||
// 2. Status s = handler.Iterate(reader, &db_id);
|
||||
// 3. Check s and handle possible errors.
|
||||
@@ -116,16 +116,17 @@ class VersionEditHandler : public VersionEditHandlerBase {
|
||||
explicit VersionEditHandler(
|
||||
bool read_only,
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
VersionSet* version_set, bool track_missing_files,
|
||||
VersionSet* version_set, bool track_found_and_missing_files,
|
||||
bool no_error_if_files_missing,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const ReadOptions& read_options,
|
||||
EpochNumberRequirement epoch_number_requirement =
|
||||
EpochNumberRequirement::kMustPresent)
|
||||
: VersionEditHandler(
|
||||
read_only, column_families, version_set, track_missing_files,
|
||||
no_error_if_files_missing, io_tracer, read_options,
|
||||
/*skip_load_table_files=*/false, epoch_number_requirement) {}
|
||||
: VersionEditHandler(read_only, column_families, version_set,
|
||||
track_found_and_missing_files,
|
||||
no_error_if_files_missing, io_tracer, read_options,
|
||||
/*skip_load_table_files=*/false,
|
||||
epoch_number_requirement) {}
|
||||
|
||||
~VersionEditHandler() override {}
|
||||
|
||||
@@ -144,7 +145,7 @@ class VersionEditHandler : public VersionEditHandlerBase {
|
||||
protected:
|
||||
explicit VersionEditHandler(
|
||||
bool read_only, std::vector<ColumnFamilyDescriptor> column_families,
|
||||
VersionSet* version_set, bool track_missing_files,
|
||||
VersionSet* version_set, bool track_found_and_missing_files,
|
||||
bool no_error_if_files_missing,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const ReadOptions& read_options, bool skip_load_table_files,
|
||||
@@ -195,7 +196,8 @@ class VersionEditHandler : public VersionEditHandlerBase {
|
||||
// by subsequent manifest records, Recover() will return failure status.
|
||||
std::unordered_map<uint32_t, std::string> column_families_not_found_;
|
||||
VersionEditParams version_edit_params_;
|
||||
const bool track_missing_files_;
|
||||
const bool track_found_and_missing_files_;
|
||||
std::unordered_map<uint32_t, std::unordered_set<uint64_t>> cf_to_found_files_;
|
||||
std::unordered_map<uint32_t, std::unordered_set<uint64_t>>
|
||||
cf_to_missing_files_;
|
||||
std::unordered_map<uint32_t, uint64_t> cf_to_missing_blob_files_high_;
|
||||
@@ -273,6 +275,8 @@ class VersionEditHandlerPointInTime : public VersionEditHandler {
|
||||
|
||||
bool in_atomic_group_ = false;
|
||||
|
||||
std::vector<std::string> intermediate_files_;
|
||||
|
||||
private:
|
||||
bool AtomicUpdateVersionsCompleted();
|
||||
bool AtomicUpdateVersionsContains(uint32_t cfid);
|
||||
@@ -310,6 +314,10 @@ class ManifestTailer : public VersionEditHandlerPointInTime {
|
||||
return cfds_changed_;
|
||||
}
|
||||
|
||||
std::vector<std::string>& GetIntermediateFiles() {
|
||||
return intermediate_files_;
|
||||
}
|
||||
|
||||
protected:
|
||||
Status Initialize() override;
|
||||
|
||||
@@ -342,7 +350,7 @@ class DumpManifestHandler : public VersionEditHandler {
|
||||
bool json)
|
||||
: VersionEditHandler(
|
||||
/*read_only=*/true, column_families, version_set,
|
||||
/*track_missing_files=*/false,
|
||||
/*track_found_and_missing_files=*/false,
|
||||
/*no_error_if_files_missing=*/false, io_tracer, read_options,
|
||||
/*skip_load_table_files=*/true),
|
||||
verbose_(verbose),
|
||||
|
||||
+38
-25
@@ -3130,6 +3130,10 @@ bool Version::MaybeInitializeFileMetaData(const ReadOptions& read_options,
|
||||
file_meta->raw_value_size = tp->raw_value_size;
|
||||
file_meta->raw_key_size = tp->raw_key_size;
|
||||
file_meta->num_range_deletions = tp->num_range_deletions;
|
||||
// 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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3141,6 +3145,7 @@ void VersionStorageInfo::UpdateAccumulatedStats(FileMetaData* file_meta) {
|
||||
accumulated_file_size_ += file_meta->fd.GetFileSize();
|
||||
accumulated_raw_key_size_ += file_meta->raw_key_size;
|
||||
accumulated_raw_value_size_ += file_meta->raw_value_size;
|
||||
assert(file_meta->num_entries >= file_meta->num_deletions);
|
||||
accumulated_num_non_deletions_ +=
|
||||
file_meta->num_entries - file_meta->num_deletions;
|
||||
accumulated_num_deletions_ += file_meta->num_deletions;
|
||||
@@ -4612,25 +4617,27 @@ uint64_t VersionStorageInfo::GetMaxEpochNumberOfFiles() const {
|
||||
return max_epoch_number;
|
||||
}
|
||||
|
||||
void VersionStorageInfo::RecoverEpochNumbers(ColumnFamilyData* cfd) {
|
||||
cfd->ResetNextEpochNumber();
|
||||
void VersionStorageInfo::RecoverEpochNumbers(ColumnFamilyData* cfd,
|
||||
bool restart_epoch, bool force) {
|
||||
if (restart_epoch) {
|
||||
cfd->ResetNextEpochNumber();
|
||||
|
||||
bool reserve_epoch_num_for_file_ingested_behind =
|
||||
cfd->ioptions()->allow_ingest_behind;
|
||||
if (reserve_epoch_num_for_file_ingested_behind) {
|
||||
uint64_t reserved_epoch_number = cfd->NewEpochNumber();
|
||||
assert(reserved_epoch_number == kReservedEpochNumberForFileIngestedBehind);
|
||||
ROCKS_LOG_INFO(cfd->ioptions()->info_log.get(),
|
||||
"[%s]CF has reserved epoch number %" PRIu64
|
||||
" for files ingested "
|
||||
"behind since `Options::allow_ingest_behind` is true",
|
||||
cfd->GetName().c_str(), reserved_epoch_number);
|
||||
bool reserve_epoch_num_for_file_ingested_behind =
|
||||
cfd->ioptions()->allow_ingest_behind;
|
||||
if (reserve_epoch_num_for_file_ingested_behind) {
|
||||
uint64_t reserved_epoch_number = cfd->NewEpochNumber();
|
||||
assert(reserved_epoch_number ==
|
||||
kReservedEpochNumberForFileIngestedBehind);
|
||||
ROCKS_LOG_INFO(cfd->ioptions()->info_log.get(),
|
||||
"[%s]CF has reserved epoch number %" PRIu64
|
||||
" for files ingested "
|
||||
"behind since `Options::allow_ingest_behind` is true",
|
||||
cfd->GetName().c_str(), reserved_epoch_number);
|
||||
}
|
||||
}
|
||||
|
||||
if (HasMissingEpochNumber()) {
|
||||
assert(epoch_number_requirement_ == EpochNumberRequirement::kMightMissing);
|
||||
assert(num_levels_ >= 1);
|
||||
|
||||
bool missing_epoch_number = HasMissingEpochNumber();
|
||||
if (missing_epoch_number || force) {
|
||||
for (int level = num_levels_ - 1; level >= 1; --level) {
|
||||
auto& files_at_level = files_[level];
|
||||
if (files_at_level.empty()) {
|
||||
@@ -4641,17 +4648,19 @@ void VersionStorageInfo::RecoverEpochNumbers(ColumnFamilyData* cfd) {
|
||||
f->epoch_number = next_epoch_number;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto file_meta_iter = files_[0].rbegin();
|
||||
file_meta_iter != files_[0].rend(); file_meta_iter++) {
|
||||
FileMetaData* f = *file_meta_iter;
|
||||
f->epoch_number = cfd->NewEpochNumber();
|
||||
}
|
||||
|
||||
ROCKS_LOG_WARN(cfd->ioptions()->info_log.get(),
|
||||
"[%s]CF's epoch numbers are inferred based on seqno",
|
||||
cfd->GetName().c_str());
|
||||
epoch_number_requirement_ = EpochNumberRequirement::kMustPresent;
|
||||
if (missing_epoch_number) {
|
||||
assert(epoch_number_requirement_ ==
|
||||
EpochNumberRequirement::kMightMissing);
|
||||
ROCKS_LOG_WARN(cfd->ioptions()->info_log.get(),
|
||||
"[%s]CF's epoch numbers are inferred based on seqno",
|
||||
cfd->GetName().c_str());
|
||||
epoch_number_requirement_ = EpochNumberRequirement::kMustPresent;
|
||||
}
|
||||
} else {
|
||||
assert(epoch_number_requirement_ == EpochNumberRequirement::kMustPresent);
|
||||
cfd->SetNextEpochNumber(
|
||||
@@ -6054,8 +6063,8 @@ Status VersionSet::Recover(
|
||||
true /* checksum */, 0 /* log_number */);
|
||||
VersionEditHandler handler(
|
||||
read_only, column_families, const_cast<VersionSet*>(this),
|
||||
/*track_missing_files=*/false, no_error_if_files_missing, io_tracer_,
|
||||
read_options, EpochNumberRequirement::kMightMissing);
|
||||
/*track_found_and_missing_files=*/false, no_error_if_files_missing,
|
||||
io_tracer_, read_options, EpochNumberRequirement::kMightMissing);
|
||||
handler.Iterate(reader, &log_read_status);
|
||||
s = handler.status();
|
||||
if (s.ok()) {
|
||||
@@ -7430,7 +7439,8 @@ Status ReactiveVersionSet::ReadAndApply(
|
||||
InstrumentedMutex* mu,
|
||||
std::unique_ptr<log::FragmentBufferedReader>* manifest_reader,
|
||||
Status* manifest_read_status,
|
||||
std::unordered_set<ColumnFamilyData*>* cfds_changed) {
|
||||
std::unordered_set<ColumnFamilyData*>* cfds_changed,
|
||||
std::vector<std::string>* files_to_delete) {
|
||||
assert(manifest_reader != nullptr);
|
||||
assert(cfds_changed != nullptr);
|
||||
mu->AssertHeld();
|
||||
@@ -7447,6 +7457,9 @@ Status ReactiveVersionSet::ReadAndApply(
|
||||
if (s.ok()) {
|
||||
*cfds_changed = std::move(manifest_tailer_->GetUpdatedColumnFamilies());
|
||||
}
|
||||
if (files_to_delete) {
|
||||
*files_to_delete = std::move(manifest_tailer_->GetIntermediateFiles());
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
+11
-2
@@ -341,7 +341,15 @@ class VersionStorageInfo {
|
||||
EpochNumberRequirement epoch_number_requirement) {
|
||||
epoch_number_requirement_ = epoch_number_requirement;
|
||||
}
|
||||
void RecoverEpochNumbers(ColumnFamilyData* cfd);
|
||||
// Ensure all files have epoch number set.
|
||||
// If there is a file missing epoch number, all files' epoch number will be
|
||||
// reset according to CF's epoch number. Otherwise, the CF will be updated
|
||||
// with the max epoch number of the files.
|
||||
//
|
||||
// @param restart_epoch This CF's epoch number will be reset to start from 0.
|
||||
// @param force Force resetting all files' epoch number.
|
||||
void RecoverEpochNumbers(ColumnFamilyData* cfd, bool restart_epoch = true,
|
||||
bool force = false);
|
||||
|
||||
class FileLocation {
|
||||
public:
|
||||
@@ -1733,7 +1741,8 @@ class ReactiveVersionSet : public VersionSet {
|
||||
InstrumentedMutex* mu,
|
||||
std::unique_ptr<log::FragmentBufferedReader>* manifest_reader,
|
||||
Status* manifest_read_status,
|
||||
std::unordered_set<ColumnFamilyData*>* cfds_changed);
|
||||
std::unordered_set<ColumnFamilyData*>* cfds_changed,
|
||||
std::vector<std::string>* files_to_delete);
|
||||
|
||||
Status Recover(const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
std::unique_ptr<log::FragmentBufferedReader>* manifest_reader,
|
||||
|
||||
+10
-5
@@ -2742,7 +2742,8 @@ TEST_F(VersionSetAtomicGroupTest,
|
||||
std::unordered_set<ColumnFamilyData*> cfds_changed;
|
||||
mu.Lock();
|
||||
EXPECT_OK(reactive_versions_->ReadAndApply(
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed));
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr));
|
||||
mu.Unlock();
|
||||
EXPECT_TRUE(first_in_atomic_group_);
|
||||
EXPECT_TRUE(last_in_atomic_group_);
|
||||
@@ -2797,7 +2798,8 @@ TEST_F(VersionSetAtomicGroupTest,
|
||||
std::unordered_set<ColumnFamilyData*> cfds_changed;
|
||||
mu.Lock();
|
||||
EXPECT_OK(reactive_versions_->ReadAndApply(
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed));
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr));
|
||||
mu.Unlock();
|
||||
// Reactive version set should be empty now.
|
||||
EXPECT_TRUE(reactive_versions_->TEST_read_edits_in_atomic_group() == 0);
|
||||
@@ -2826,7 +2828,8 @@ TEST_F(VersionSetAtomicGroupTest,
|
||||
std::unordered_set<ColumnFamilyData*> cfds_changed;
|
||||
mu.Lock();
|
||||
EXPECT_OK(reactive_versions_->ReadAndApply(
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed));
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr));
|
||||
mu.Unlock();
|
||||
EXPECT_TRUE(first_in_atomic_group_);
|
||||
EXPECT_FALSE(last_in_atomic_group_);
|
||||
@@ -2882,7 +2885,8 @@ TEST_F(VersionSetAtomicGroupTest,
|
||||
AddNewEditsToLog(kAtomicGroupSize);
|
||||
mu.Lock();
|
||||
EXPECT_NOK(reactive_versions_->ReadAndApply(
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed));
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr));
|
||||
mu.Unlock();
|
||||
EXPECT_EQ(edits_[kAtomicGroupSize / 2].DebugString(),
|
||||
corrupted_edit_.DebugString());
|
||||
@@ -2932,7 +2936,8 @@ TEST_F(VersionSetAtomicGroupTest,
|
||||
AddNewEditsToLog(kAtomicGroupSize);
|
||||
mu.Lock();
|
||||
EXPECT_NOK(reactive_versions_->ReadAndApply(
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed));
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr));
|
||||
mu.Unlock();
|
||||
EXPECT_EQ(edits_[1].DebugString(),
|
||||
edit_with_incorrect_group_size_.DebugString());
|
||||
|
||||
@@ -1794,6 +1794,144 @@ TEST_F(DBWideBasicTest, PinnableWideColumnsMove) {
|
||||
test_move(/* fill_cache*/ true);
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, SanityChecks) {
|
||||
constexpr char foo[] = "foo";
|
||||
constexpr char bar[] = "bar";
|
||||
constexpr size_t num_keys = 2;
|
||||
|
||||
{
|
||||
constexpr ColumnFamilyHandle* column_family = nullptr;
|
||||
PinnableWideColumns columns;
|
||||
ASSERT_TRUE(db_->GetEntity(ReadOptions(), column_family, foo, &columns)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
constexpr PinnableWideColumns* columns = nullptr;
|
||||
ASSERT_TRUE(
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), foo, columns)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
ReadOptions read_options;
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
|
||||
PinnableWideColumns columns;
|
||||
ASSERT_TRUE(
|
||||
db_->GetEntity(read_options, db_->DefaultColumnFamily(), foo, &columns)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
constexpr ColumnFamilyHandle* column_family = nullptr;
|
||||
std::array<Slice, num_keys> keys{{foo, bar}};
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), column_family, num_keys, keys.data(),
|
||||
results.data(), statuses.data());
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
constexpr Slice* keys = nullptr;
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), num_keys,
|
||||
keys, results.data(), statuses.data());
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
std::array<Slice, num_keys> keys{{foo, bar}};
|
||||
constexpr PinnableWideColumns* results = nullptr;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), num_keys,
|
||||
keys.data(), results, statuses.data());
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
ReadOptions read_options;
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
|
||||
std::array<Slice, num_keys> keys{{foo, bar}};
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(read_options, db_->DefaultColumnFamily(), num_keys,
|
||||
keys.data(), results.data(), statuses.data());
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
constexpr ColumnFamilyHandle** column_families = nullptr;
|
||||
std::array<Slice, num_keys> keys{{foo, bar}};
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), num_keys, column_families, keys.data(),
|
||||
results.data(), statuses.data());
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
std::array<ColumnFamilyHandle*, num_keys> column_families{
|
||||
{db_->DefaultColumnFamily(), db_->DefaultColumnFamily()}};
|
||||
constexpr Slice* keys = nullptr;
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), num_keys, column_families.data(), keys,
|
||||
results.data(), statuses.data());
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
std::array<ColumnFamilyHandle*, num_keys> column_families{
|
||||
{db_->DefaultColumnFamily(), db_->DefaultColumnFamily()}};
|
||||
std::array<Slice, num_keys> keys{{foo, bar}};
|
||||
constexpr PinnableWideColumns* results = nullptr;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), num_keys, column_families.data(),
|
||||
keys.data(), results, statuses.data());
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
|
||||
{
|
||||
ReadOptions read_options;
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
|
||||
std::array<ColumnFamilyHandle*, num_keys> column_families{
|
||||
{db_->DefaultColumnFamily(), db_->DefaultColumnFamily()}};
|
||||
std::array<Slice, num_keys> keys{{foo, bar}};
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(read_options, num_keys, column_families.data(),
|
||||
keys.data(), results.data(), statuses.data());
|
||||
ASSERT_TRUE(statuses[0].IsInvalidArgument());
|
||||
ASSERT_TRUE(statuses[1].IsInvalidArgument());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -54,7 +54,12 @@ class BatchedOpsStressTest : public StressTest {
|
||||
const std::string v = value_body + num;
|
||||
if (FLAGS_use_put_entity_one_in > 0 &&
|
||||
(value_base % FLAGS_use_put_entity_one_in) == 0) {
|
||||
status = batch.PutEntity(cfh, k, GenerateWideColumns(value_base, v));
|
||||
if (FLAGS_use_attribute_group) {
|
||||
status =
|
||||
batch.PutEntity(k, GenerateAttributeGroups({cfh}, value_base, v));
|
||||
} else {
|
||||
status = batch.PutEntity(cfh, k, GenerateWideColumns(value_base, v));
|
||||
}
|
||||
} else if (FLAGS_use_timed_put_one_in > 0 &&
|
||||
((value_base + kLargePrimeForCommonFactorSkew) %
|
||||
FLAGS_use_timed_put_one_in) == 0) {
|
||||
@@ -300,15 +305,30 @@ class BatchedOpsStressTest : public StressTest {
|
||||
|
||||
constexpr size_t num_keys = 10;
|
||||
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<PinnableWideColumns, num_keys> column_results;
|
||||
std::array<PinnableAttributeGroups, num_keys> attribute_group_results;
|
||||
|
||||
std::string error_msg_header = FLAGS_use_attribute_group
|
||||
? "GetEntity (AttributeGroup) error"
|
||||
: "GetEntity error";
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const std::string key = std::to_string(i) + key_suffix;
|
||||
|
||||
const Status s = db_->GetEntity(read_opts_copy, cfh, key, &results[i]);
|
||||
Status s;
|
||||
if (FLAGS_use_attribute_group) {
|
||||
attribute_group_results[i].emplace_back(cfh);
|
||||
s = db_->GetEntity(read_opts_copy, key, &attribute_group_results[i]);
|
||||
if (s.ok()) {
|
||||
s = attribute_group_results[i].back().status();
|
||||
}
|
||||
} else {
|
||||
s = db_->GetEntity(read_opts_copy, cfh, key, &column_results[i]);
|
||||
}
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "GetEntity error: %s\n", s.ToString().c_str());
|
||||
fprintf(stderr, "%s: %s\n", error_msg_header.c_str(),
|
||||
s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
} else if (s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
@@ -317,14 +337,20 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const WideColumns& columns = results[i].columns();
|
||||
const WideColumns& columns_to_compare =
|
||||
FLAGS_use_attribute_group ? attribute_group_results[0].front().columns()
|
||||
: column_results[0].columns();
|
||||
|
||||
if (!CompareColumns(results[0].columns(), columns)) {
|
||||
fprintf(stderr,
|
||||
"GetEntity error: inconsistent entities for key %s: %s, %s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(results[0].columns()).c_str(),
|
||||
for (size_t i = 1; i < num_keys; ++i) {
|
||||
const WideColumns& columns =
|
||||
FLAGS_use_attribute_group
|
||||
? attribute_group_results[i].front().columns()
|
||||
: column_results[i].columns();
|
||||
|
||||
if (!CompareColumns(columns_to_compare, columns)) {
|
||||
fprintf(stderr, "%s: inconsistent entities for key %s: %s, %s\n",
|
||||
error_msg_header.c_str(), StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns_to_compare).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
|
||||
@@ -338,20 +364,18 @@ class BatchedOpsStressTest : public StressTest {
|
||||
|
||||
if (value.empty() || value[value.size() - 1] != expected) {
|
||||
fprintf(stderr,
|
||||
"GetEntity error: incorrect column value for key "
|
||||
"%s: incorrect column value for key "
|
||||
"%s, entity %s, column value %s, expected %c\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
error_msg_header.c_str(), StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str(),
|
||||
value.ToString(/* hex */ true).c_str(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"GetEntity error: inconsistent columns for key %s, entity %s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
fprintf(stderr, "%s: inconsistent columns for key %s, entity %s\n",
|
||||
error_msg_header.c_str(), StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,66 +409,140 @@ class BatchedOpsStressTest : public StressTest {
|
||||
|
||||
std::array<std::string, num_prefixes> keys;
|
||||
std::array<Slice, num_prefixes> key_slices;
|
||||
std::array<PinnableWideColumns, num_prefixes> results;
|
||||
std::array<Status, num_prefixes> statuses;
|
||||
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
keys[j] = std::to_string(j) + key_suffix;
|
||||
key_slices[j] = keys[j];
|
||||
}
|
||||
|
||||
db_->MultiGetEntity(read_opts_copy, cfh, num_prefixes, key_slices.data(),
|
||||
results.data(), statuses.data());
|
||||
if (FLAGS_use_attribute_group) {
|
||||
// AttributeGroup MultiGetEntity verification
|
||||
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
const Status& s = statuses[j];
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "MultiGetEntity error: %s\n", s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
} else if (s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
} else {
|
||||
thread->stats.AddGets(1, 1);
|
||||
std::vector<PinnableAttributeGroups> results;
|
||||
results.reserve(num_prefixes);
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
PinnableAttributeGroups attribute_groups;
|
||||
attribute_groups.emplace_back(cfh);
|
||||
results.emplace_back(std::move(attribute_groups));
|
||||
}
|
||||
db_->MultiGetEntity(read_opts_copy, num_prefixes, key_slices.data(),
|
||||
results.data());
|
||||
|
||||
const WideColumns& cmp_columns = results[0].columns();
|
||||
const WideColumns& columns = results[j].columns();
|
||||
const WideColumns& cmp_columns = results[0][0].columns();
|
||||
|
||||
if (!CompareColumns(cmp_columns, columns)) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: inconsistent entities for key %s: %s, "
|
||||
"%s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
|
||||
if (!columns.empty()) {
|
||||
// The last character of each column value should be 'j' as a decimal
|
||||
// digit
|
||||
const char expected = static_cast<char>('0' + j);
|
||||
|
||||
for (const auto& column : columns) {
|
||||
const Slice& value = column.value();
|
||||
|
||||
if (value.empty() || value[value.size() - 1] != expected) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: incorrect column value for key "
|
||||
"%s, entity %s, column value %s, expected %c\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str(),
|
||||
value.ToString(/* hex */ true).c_str(), expected);
|
||||
}
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
const auto& attribute_groups = results[j];
|
||||
assert(attribute_groups.size() == 1);
|
||||
const Status& s = attribute_groups[0].status();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "MultiGetEntity (AttributeGroup) error: %s\n",
|
||||
s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
} else if (s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
} else {
|
||||
thread->stats.AddGets(1, 1);
|
||||
}
|
||||
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
const WideColumns& columns = results[j][0].columns();
|
||||
if (!CompareColumns(cmp_columns, columns)) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: inconsistent columns for key %s, "
|
||||
"entity %s\n",
|
||||
"MultiGetEntity (AttributeGroup) error: inconsistent "
|
||||
"entities for key %s: %s, "
|
||||
"%s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
if (!columns.empty()) {
|
||||
// The last character of each column value should be 'j' as a
|
||||
// decimal digit
|
||||
const char expected = static_cast<char>('0' + j);
|
||||
|
||||
for (const auto& column : columns) {
|
||||
const Slice& value = column.value();
|
||||
|
||||
if (value.empty() || value[value.size() - 1] != expected) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) error: incorrect "
|
||||
"column value for key "
|
||||
"%s, entity %s, column value %s, expected %c\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str(),
|
||||
value.ToString(/* hex */ true).c_str(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) error: inconsistent "
|
||||
"columns for key %s, "
|
||||
"entity %s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
std::array<PinnableWideColumns, num_prefixes> results;
|
||||
std::array<Status, num_prefixes> statuses;
|
||||
|
||||
db_->MultiGetEntity(read_opts_copy, cfh, num_prefixes,
|
||||
key_slices.data(), results.data(), statuses.data());
|
||||
|
||||
const WideColumns& cmp_columns = results[0].columns();
|
||||
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
const Status& s = statuses[j];
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "MultiGetEntity error: %s\n", s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
} else if (s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
} else {
|
||||
thread->stats.AddGets(1, 1);
|
||||
}
|
||||
const WideColumns& columns = results[j].columns();
|
||||
|
||||
if (!CompareColumns(cmp_columns, columns)) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"MultiGetEntity error: inconsistent entities for key %s: %s, "
|
||||
"%s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
|
||||
if (!columns.empty()) {
|
||||
// The last character of each column value should be 'j' as a
|
||||
// decimal digit
|
||||
const char expected = static_cast<char>('0' + j);
|
||||
|
||||
for (const auto& column : columns) {
|
||||
const Slice& value = column.value();
|
||||
|
||||
if (value.empty() || value[value.size() - 1] != expected) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: incorrect column value for key "
|
||||
"%s, entity %s, column value %s, expected %c\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str(),
|
||||
value.ToString(/* hex */ true).c_str(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: inconsistent columns for key %s, "
|
||||
"entity %s\n",
|
||||
StringToHex(key_suffix).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,25 +37,35 @@ class CfConsistencyStressTest : public StressTest {
|
||||
WriteBatch batch;
|
||||
|
||||
Status status;
|
||||
for (auto cf : rand_column_families) {
|
||||
ColumnFamilyHandle* const cfh = column_families_[cf];
|
||||
assert(cfh);
|
||||
|
||||
if (FLAGS_use_put_entity_one_in > 0 &&
|
||||
(value_base % FLAGS_use_put_entity_one_in) == 0) {
|
||||
status = batch.PutEntity(cfh, k, GenerateWideColumns(value_base, v));
|
||||
} else if (FLAGS_use_timed_put_one_in > 0 &&
|
||||
((value_base + kLargePrimeForCommonFactorSkew) %
|
||||
FLAGS_use_timed_put_one_in) == 0) {
|
||||
uint64_t write_unix_time = GetWriteUnixTime(thread);
|
||||
status = batch.TimedPut(cfh, k, v, write_unix_time);
|
||||
} else if (FLAGS_use_merge) {
|
||||
status = batch.Merge(cfh, k, v);
|
||||
} else {
|
||||
status = batch.Put(cfh, k, v);
|
||||
if (FLAGS_use_attribute_group && FLAGS_use_put_entity_one_in > 0 &&
|
||||
(value_base % FLAGS_use_put_entity_one_in) == 0) {
|
||||
std::vector<ColumnFamilyHandle*> cfhs;
|
||||
cfhs.reserve(rand_column_families.size());
|
||||
for (auto cf : rand_column_families) {
|
||||
cfhs.push_back(column_families_[cf]);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
status = batch.PutEntity(k, GenerateAttributeGroups(cfhs, value_base, v));
|
||||
} else {
|
||||
for (auto cf : rand_column_families) {
|
||||
ColumnFamilyHandle* const cfh = column_families_[cf];
|
||||
assert(cfh);
|
||||
|
||||
if (FLAGS_use_put_entity_one_in > 0 &&
|
||||
(value_base % FLAGS_use_put_entity_one_in) == 0) {
|
||||
status = batch.PutEntity(cfh, k, GenerateWideColumns(value_base, v));
|
||||
} else if (FLAGS_use_timed_put_one_in > 0 &&
|
||||
((value_base + kLargePrimeForCommonFactorSkew) %
|
||||
FLAGS_use_timed_put_one_in) == 0) {
|
||||
uint64_t write_unix_time = GetWriteUnixTime(thread);
|
||||
status = batch.TimedPut(cfh, k, v, write_unix_time);
|
||||
} else if (FLAGS_use_merge) {
|
||||
status = batch.Merge(cfh, k, v);
|
||||
} else {
|
||||
status = batch.Put(cfh, k, v);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,57 +338,122 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
|
||||
if (is_consistent) {
|
||||
for (size_t i = 1; i < rand_column_families.size(); ++i) {
|
||||
assert(rand_column_families[i] >= 0);
|
||||
assert(rand_column_families[i] <
|
||||
static_cast<int>(column_families_.size()));
|
||||
if (FLAGS_use_attribute_group) {
|
||||
PinnableAttributeGroups result;
|
||||
result.reserve(rand_column_families.size());
|
||||
for (size_t i = 1; i < rand_column_families.size(); ++i) {
|
||||
assert(rand_column_families[i] >= 0);
|
||||
assert(rand_column_families[i] <
|
||||
static_cast<int>(column_families_.size()));
|
||||
|
||||
PinnableWideColumns result;
|
||||
s = db_->GetEntity(read_opts_copy,
|
||||
column_families_[rand_column_families[i]], key,
|
||||
&result);
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
break;
|
||||
result.emplace_back(column_families_[rand_column_families[i]]);
|
||||
}
|
||||
s = db_->GetEntity(read_opts_copy, key, &result);
|
||||
if (s.ok()) {
|
||||
for (auto& attribute_group : result) {
|
||||
s = attribute_group.status();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bool found = s.ok();
|
||||
const bool found = s.ok();
|
||||
|
||||
assert(!column_family_names_.empty());
|
||||
assert(i < column_family_names_.size());
|
||||
|
||||
if (!cmp_found && found) {
|
||||
fprintf(stderr,
|
||||
"GetEntity returns different results for key %s: CF %s "
|
||||
"returns not found, CF %s returns entity %s\n",
|
||||
if (!cmp_found && found) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Non-AttributeGroup GetEntity returns different results "
|
||||
"than AttributeGroup GetEntity for key %s: CF %s "
|
||||
"returns not found, CF %s returns entity %s \n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
column_family_names_[i].c_str(),
|
||||
WideColumnsToHex(result.columns()).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmp_found && !found) {
|
||||
fprintf(stderr,
|
||||
"GetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns not found\n",
|
||||
attribute_group.column_family()->GetName().c_str(),
|
||||
WideColumnsToHex(attribute_group.columns()).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
if (cmp_found && !found) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Non-AttributeGroup GetEntity returns different results "
|
||||
"than AttributeGroup GetEntity for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns not found \n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str(),
|
||||
column_family_names_[i].c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (found && result != cmp_result) {
|
||||
fprintf(stderr,
|
||||
"GetEntity returns different results for key %s: CF %s "
|
||||
attribute_group.column_family()->GetName().c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
if (found &&
|
||||
attribute_group.columns() != cmp_result.columns()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Non-AttributeGroup GetEntity returns different results "
|
||||
"than AttributeGroup GetEntity for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str(),
|
||||
column_family_names_[i].c_str(),
|
||||
WideColumnsToHex(result.columns()).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
attribute_group.column_family()->GetName().c_str(),
|
||||
WideColumnsToHex(attribute_group.columns()).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 1; i < rand_column_families.size(); ++i) {
|
||||
assert(rand_column_families[i] >= 0);
|
||||
assert(rand_column_families[i] <
|
||||
static_cast<int>(column_families_.size()));
|
||||
|
||||
PinnableWideColumns result;
|
||||
s = db_->GetEntity(read_opts_copy,
|
||||
column_families_[rand_column_families[i]], key,
|
||||
&result);
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bool found = s.ok();
|
||||
|
||||
assert(!column_family_names_.empty());
|
||||
assert(i < column_family_names_.size());
|
||||
|
||||
if (!cmp_found && found) {
|
||||
fprintf(stderr,
|
||||
"GetEntity returns different results for key %s: CF %s "
|
||||
"returns not found, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(),
|
||||
column_family_names_[0].c_str(),
|
||||
column_family_names_[i].c_str(),
|
||||
WideColumnsToHex(result.columns()).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmp_found && !found) {
|
||||
fprintf(stderr,
|
||||
"GetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns not found\n",
|
||||
StringToHex(key).c_str(),
|
||||
column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str(),
|
||||
column_family_names_[i].c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (found && result != cmp_result) {
|
||||
fprintf(stderr,
|
||||
"GetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(),
|
||||
column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str(),
|
||||
column_family_names_[i].c_str(),
|
||||
WideColumnsToHex(result.columns()).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,94 +506,219 @@ class CfConsistencyStressTest : public StressTest {
|
||||
|
||||
const size_t num_keys = rand_keys.size();
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const std::string key = Key(rand_keys[i]);
|
||||
if (FLAGS_use_attribute_group) {
|
||||
// AttributeGroup MultiGetEntity verification
|
||||
|
||||
std::vector<Slice> key_slices(num_cfs, key);
|
||||
std::vector<PinnableWideColumns> results(num_cfs);
|
||||
std::vector<Status> statuses(num_cfs);
|
||||
std::vector<PinnableAttributeGroups> results;
|
||||
std::vector<Slice> key_slices;
|
||||
std::vector<std::string> key_strs;
|
||||
results.reserve(num_keys);
|
||||
key_slices.reserve(num_keys);
|
||||
key_strs.reserve(num_keys);
|
||||
|
||||
db_->MultiGetEntity(read_opts_copy, num_cfs, cfhs.data(),
|
||||
key_slices.data(), results.data(), statuses.data());
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
key_strs.emplace_back(Key(rand_keys[i]));
|
||||
key_slices.emplace_back(key_strs.back());
|
||||
PinnableAttributeGroups attribute_groups;
|
||||
for (auto* cfh : cfhs) {
|
||||
attribute_groups.emplace_back(cfh);
|
||||
}
|
||||
results.emplace_back(std::move(attribute_groups));
|
||||
}
|
||||
db_->MultiGetEntity(read_opts_copy, num_keys, key_slices.data(),
|
||||
results.data());
|
||||
|
||||
bool is_consistent = true;
|
||||
|
||||
for (size_t j = 0; j < num_cfs; ++j) {
|
||||
const Status& s = statuses[j];
|
||||
const Status& cmp_s = statuses[0];
|
||||
const WideColumns& columns = results[j].columns();
|
||||
const WideColumns& cmp_columns = results[0].columns();
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const auto& result = results[i];
|
||||
const Status& cmp_s = result[0].status();
|
||||
const WideColumns& cmp_columns = result[0].columns();
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "TestMultiGetEntity error: %s\n",
|
||||
s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
break;
|
||||
}
|
||||
bool has_error = false;
|
||||
|
||||
assert(cmp_s.ok() || cmp_s.IsNotFound());
|
||||
for (size_t j = 0; j < num_cfs; ++j) {
|
||||
const Status& s = result[j].status();
|
||||
const WideColumns& columns = result[j].columns();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "TestMultiGetEntity (AttributeGroup) error: %s\n",
|
||||
s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
has_error = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
if (cmp_s.ok()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"MultiGetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns not found\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
column_family_names_[j].c_str());
|
||||
assert(cmp_s.ok() || cmp_s.IsNotFound());
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
if (cmp_s.ok()) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) returns different "
|
||||
"results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns not found\n",
|
||||
key_slices[i].ToString(true).c_str(),
|
||||
column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
column_family_names_[j].c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) returns different results "
|
||||
"for key %s: CF %s "
|
||||
"returns not found, CF %s returns entity %s\n",
|
||||
key_slices[i].ToString(true).c_str(),
|
||||
column_family_names_[0].c_str(),
|
||||
column_family_names_[j].c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
if (columns != cmp_columns) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) returns different results "
|
||||
"for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns entity %s\n",
|
||||
key_slices[i].ToString(true).c_str(),
|
||||
column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
column_family_names_[j].c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity returns different results for key %s: CF %s "
|
||||
"returns not found, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
column_family_names_[j].c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) error: inconsistent "
|
||||
"columns for key %s, "
|
||||
"entity %s\n",
|
||||
key_slices[i].ToString(true).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (columns != cmp_columns) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
column_family_names_[j].c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
if (has_error) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
} else if (!is_consistent) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: inconsistent columns for key %s, "
|
||||
"entity %s\n",
|
||||
StringToHex(key).c_str(), WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
"TestMultiGetEntity (AttributeGroup) error: results are not "
|
||||
"consistent\n");
|
||||
thread->stats.AddErrors(1);
|
||||
// Fail fast to preserve the DB state.
|
||||
thread->shared->SetVerificationFailure();
|
||||
break;
|
||||
} else if (cmp_s.ok()) {
|
||||
thread->stats.AddGets(1, 1);
|
||||
} else if (cmp_s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_consistent) {
|
||||
fprintf(stderr,
|
||||
"TestMultiGetEntity error: results are not consistent\n");
|
||||
thread->stats.AddErrors(1);
|
||||
// Fail fast to preserve the DB state.
|
||||
thread->shared->SetVerificationFailure();
|
||||
break;
|
||||
} else if (statuses[0].ok()) {
|
||||
thread->stats.AddGets(1, 1);
|
||||
} else if (statuses[0].IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const std::string key = Key(rand_keys[i]);
|
||||
|
||||
std::vector<Slice> key_slices(num_cfs, key);
|
||||
std::vector<PinnableWideColumns> results(num_cfs);
|
||||
std::vector<Status> statuses(num_cfs);
|
||||
|
||||
db_->MultiGetEntity(read_opts_copy, num_cfs, cfhs.data(),
|
||||
key_slices.data(), results.data(), statuses.data());
|
||||
|
||||
bool is_consistent = true;
|
||||
|
||||
const Status& cmp_s = statuses[0];
|
||||
const WideColumns& cmp_columns = results[0].columns();
|
||||
|
||||
for (size_t j = 0; j < num_cfs; ++j) {
|
||||
const Status& s = statuses[j];
|
||||
const WideColumns& columns = results[j].columns();
|
||||
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "TestMultiGetEntity error: %s\n",
|
||||
s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(cmp_s.ok() || cmp_s.IsNotFound());
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
if (cmp_s.ok()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"MultiGetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns not found\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
column_family_names_[j].c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"MultiGetEntity returns different results for key %s: CF %s "
|
||||
"returns not found, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
column_family_names_[j].c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (columns != cmp_columns) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"MultiGetEntity returns different results for key %s: CF %s "
|
||||
"returns entity %s, CF %s returns entity %s\n",
|
||||
StringToHex(key).c_str(), column_family_names_[0].c_str(),
|
||||
WideColumnsToHex(cmp_columns).c_str(),
|
||||
column_family_names_[j].c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity error: inconsistent columns for key %s, "
|
||||
"entity %s\n",
|
||||
StringToHex(key).c_str(),
|
||||
WideColumnsToHex(columns).c_str());
|
||||
is_consistent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_consistent) {
|
||||
fprintf(stderr,
|
||||
"TestMultiGetEntity error: results are not consistent\n");
|
||||
thread->stats.AddErrors(1);
|
||||
// Fail fast to preserve the DB state.
|
||||
thread->shared->SetVerificationFailure();
|
||||
break;
|
||||
} else if (statuses[0].ok()) {
|
||||
thread->stats.AddGets(1, 1);
|
||||
} else if (statuses[0].IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,17 @@ uint32_t GetValueBase(Slice s) {
|
||||
return res;
|
||||
}
|
||||
|
||||
AttributeGroups GenerateAttributeGroups(
|
||||
const std::vector<ColumnFamilyHandle*>& cfhs, uint32_t value_base,
|
||||
const Slice& slice) {
|
||||
WideColumns columns = GenerateWideColumns(value_base, slice);
|
||||
AttributeGroups attribute_groups;
|
||||
for (auto* cfh : cfhs) {
|
||||
attribute_groups.emplace_back(cfh, columns);
|
||||
}
|
||||
return attribute_groups;
|
||||
}
|
||||
|
||||
WideColumns GenerateWideColumns(uint32_t value_base, const Slice& slice) {
|
||||
WideColumns columns;
|
||||
|
||||
|
||||
@@ -209,8 +209,11 @@ DECLARE_int32(ingest_external_file_one_in);
|
||||
DECLARE_int32(ingest_external_file_width);
|
||||
DECLARE_int32(compact_files_one_in);
|
||||
DECLARE_int32(compact_range_one_in);
|
||||
DECLARE_int32(promote_l0_one_in);
|
||||
DECLARE_int32(mark_for_compaction_one_file_in);
|
||||
DECLARE_int32(flush_one_in);
|
||||
DECLARE_int32(key_may_exist_one_in);
|
||||
DECLARE_int32(reset_stats_one_in);
|
||||
DECLARE_int32(pause_background_one_in);
|
||||
DECLARE_int32(disable_file_deletions_one_in);
|
||||
DECLARE_int32(disable_manual_compaction_one_in);
|
||||
@@ -250,6 +253,7 @@ DECLARE_string(memtablerep);
|
||||
DECLARE_int32(prefix_size);
|
||||
DECLARE_bool(use_merge);
|
||||
DECLARE_uint32(use_put_entity_one_in);
|
||||
DECLARE_bool(use_attribute_group);
|
||||
DECLARE_bool(use_full_merge_v1);
|
||||
DECLARE_int32(sync_wal_one_in);
|
||||
DECLARE_bool(avoid_unnecessary_blocking_io);
|
||||
@@ -758,6 +762,10 @@ WideColumns GenerateExpectedWideColumns(uint32_t value_base,
|
||||
bool VerifyWideColumns(const Slice& value, const WideColumns& columns);
|
||||
bool VerifyWideColumns(const WideColumns& columns);
|
||||
|
||||
AttributeGroups GenerateAttributeGroups(
|
||||
const std::vector<ColumnFamilyHandle*>& cfhs, uint32_t value_base,
|
||||
const Slice& slice);
|
||||
|
||||
StressTest* CreateCfConsistencyStressTest();
|
||||
StressTest* CreateBatchedOpsStressTest();
|
||||
StressTest* CreateNonBatchedOpsStressTest();
|
||||
|
||||
@@ -756,6 +756,10 @@ DEFINE_int32(compact_range_one_in, 0,
|
||||
"If non-zero, then CompactRange() will be called once for every N "
|
||||
"operations on average. 0 indicates CompactRange() is disabled.");
|
||||
|
||||
DEFINE_int32(promote_l0_one_in, 0,
|
||||
"If non-zero, then PromoteL0() will be called once for every N "
|
||||
"operations on average. 0 indicates PromoteL0() is disabled.");
|
||||
|
||||
DEFINE_int32(mark_for_compaction_one_file_in, 0,
|
||||
"A `TablePropertiesCollectorFactory` will be registered, which "
|
||||
"creates a `TablePropertiesCollector` with `NeedCompact()` "
|
||||
@@ -766,6 +770,14 @@ DEFINE_int32(flush_one_in, 0,
|
||||
"If non-zero, then Flush() will be called once for every N ops "
|
||||
"on average. 0 indicates calls to Flush() are disabled.");
|
||||
|
||||
DEFINE_int32(key_may_exist_one_in, 0,
|
||||
"If non-zero, then KeyMayExist() will be called "
|
||||
"once for every N ops on average. 0 disables.");
|
||||
|
||||
DEFINE_int32(reset_stats_one_in, 0,
|
||||
"If non-zero, then ResetStats() will be called "
|
||||
"once for every N ops on average. 0 disables.");
|
||||
|
||||
DEFINE_int32(pause_background_one_in, 0,
|
||||
"If non-zero, then PauseBackgroundWork()+Continue will be called "
|
||||
"once for every N ops on average. 0 disables.");
|
||||
@@ -940,6 +952,9 @@ DEFINE_uint32(use_put_entity_one_in, 0,
|
||||
"If greater than zero, PutEntity will be used once per every N "
|
||||
"write ops on average.");
|
||||
|
||||
DEFINE_bool(use_attribute_group, false,
|
||||
"If set, use the attribute_group API to put/get entities");
|
||||
|
||||
DEFINE_bool(use_full_merge_v1, false,
|
||||
"On true, use a merge operator that implement the deprecated "
|
||||
"version of FullMerge");
|
||||
|
||||
@@ -505,11 +505,13 @@ Status StressTest::AssertSame(DB* db, ColumnFamilyHandle* cf,
|
||||
}
|
||||
|
||||
void StressTest::ProcessStatus(SharedState* shared, std::string opname,
|
||||
Status s) const {
|
||||
const Status& s,
|
||||
bool ignore_injected_error) const {
|
||||
if (s.ok()) {
|
||||
return;
|
||||
}
|
||||
if (!s.IsIOError() || !std::strstr(s.getState(), "injected")) {
|
||||
if (!s.IsIOError() || !std::strstr(s.getState(), "injected") ||
|
||||
!ignore_injected_error) {
|
||||
std::ostringstream oss;
|
||||
oss << opname << " failed: " << s.ToString();
|
||||
VerificationAbort(shared, oss.str());
|
||||
@@ -956,12 +958,48 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "LockWAL() failed: %s\n", s.ToString().c_str());
|
||||
} else {
|
||||
// Verify no writes during LockWAL
|
||||
auto old_seqno = db_->GetLatestSequenceNumber();
|
||||
// Yield for a while
|
||||
do {
|
||||
std::this_thread::yield();
|
||||
} while (thread->rand.OneIn(2));
|
||||
// Latest seqno should not have changed
|
||||
// And also that WAL is not changed during LockWAL()
|
||||
std::unique_ptr<LogFile> old_wal;
|
||||
s = db_->GetCurrentWalFile(&old_wal);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "GetCurrentWalFile() failed: %s\n",
|
||||
s.ToString().c_str());
|
||||
} else {
|
||||
// Yield for a while
|
||||
do {
|
||||
std::this_thread::yield();
|
||||
} while (thread->rand.OneIn(2));
|
||||
// Current WAL and size should not have changed
|
||||
std::unique_ptr<LogFile> new_wal;
|
||||
s = db_->GetCurrentWalFile(&new_wal);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "GetCurrentWalFile() failed: %s\n",
|
||||
s.ToString().c_str());
|
||||
} else {
|
||||
if (old_wal->LogNumber() != new_wal->LogNumber()) {
|
||||
fprintf(stderr,
|
||||
"Failed: WAL number changed during LockWAL(): %" PRIu64
|
||||
" to %" PRIu64 "\n",
|
||||
old_wal->LogNumber(), new_wal->LogNumber());
|
||||
}
|
||||
// FIXME: FaultInjectionTestFS does not report file sizes that
|
||||
// reflect what has been flushed. Either that needs to be fixed
|
||||
// or GetSortedWals/GetLiveWalFile need to stop relying on
|
||||
// asking the FS for sizes.
|
||||
if (!fault_fs_guard &&
|
||||
old_wal->SizeFileBytes() != new_wal->SizeFileBytes()) {
|
||||
fprintf(stderr,
|
||||
"Failed: WAL %" PRIu64
|
||||
" size changed during LockWAL(): %" PRIu64
|
||||
" to %" PRIu64 "\n",
|
||||
old_wal->LogNumber(), old_wal->SizeFileBytes(),
|
||||
new_wal->SizeFileBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Verify no writes during LockWAL
|
||||
auto new_seqno = db_->GetLatestSequenceNumber();
|
||||
if (old_seqno != new_seqno) {
|
||||
fprintf(
|
||||
@@ -969,6 +1007,7 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
"Failure: latest seqno changed from %u to %u with WAL locked\n",
|
||||
(unsigned)old_seqno, (unsigned)new_seqno);
|
||||
}
|
||||
// Verification done. Now unlock WAL
|
||||
s = db_->UnlockWAL();
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "UnlockWAL() failed: %s\n", s.ToString().c_str());
|
||||
@@ -1001,6 +1040,10 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
}
|
||||
}
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_promote_l0_one_in)) {
|
||||
TestPromoteL0(thread, column_family);
|
||||
}
|
||||
|
||||
std::vector<int> rand_column_families =
|
||||
GenerateColumnFamilies(FLAGS_column_families, rand_column_family);
|
||||
|
||||
@@ -1041,6 +1084,11 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
ProcessStatus(shared, "VerifyGetCurrentWalFile", status);
|
||||
}
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_reset_stats_one_in)) {
|
||||
Status status = TestResetStats();
|
||||
ProcessStatus(shared, "ResetStats", status);
|
||||
}
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_pause_background_one_in)) {
|
||||
Status status = TestPauseBackground(thread);
|
||||
ProcessStatus(shared, "Pause/ContinueBackgroundWork", status);
|
||||
@@ -1135,6 +1183,10 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
read_opts.timestamp = &read_ts;
|
||||
}
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_key_may_exist_one_in)) {
|
||||
TestKeyMayExist(thread, read_opts, rand_column_families, rand_keys);
|
||||
}
|
||||
|
||||
int prob_op = thread->rand.Uniform(100);
|
||||
// Reset this in case we pick something other than a read op. We don't
|
||||
// want to use a stale value when deciding at the beginning of the loop
|
||||
@@ -2428,8 +2480,32 @@ void StressTest::TestCompactFiles(ThreadState* thread,
|
||||
}
|
||||
}
|
||||
|
||||
void StressTest::TestPromoteL0(ThreadState* thread,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
int target_level = thread->rand.Next() % options_.num_levels;
|
||||
Status s = db_->PromoteL0(column_family, target_level);
|
||||
if (!s.ok()) {
|
||||
// The second error occurs when another concurrent PromoteL0() moving the
|
||||
// same files finishes first which is an allowed behavior
|
||||
bool non_ok_status_allowed =
|
||||
s.IsInvalidArgument() ||
|
||||
(s.IsCorruption() &&
|
||||
s.ToString().find("VersionBuilder: Cannot delete table file") !=
|
||||
std::string::npos &&
|
||||
s.ToString().find("since it is on level") != std::string::npos);
|
||||
fprintf(non_ok_status_allowed ? stdout : stderr,
|
||||
"Unable to perform PromoteL0(): %s under specified "
|
||||
"target_level: %d.\n",
|
||||
s.ToString().c_str(), target_level);
|
||||
if (!non_ok_status_allowed) {
|
||||
thread->shared->SafeTerminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status StressTest::TestFlush(const std::vector<int>& rand_column_families) {
|
||||
FlushOptions flush_opts;
|
||||
assert(flush_opts.wait);
|
||||
if (FLAGS_atomic_flush) {
|
||||
return db_->Flush(flush_opts, column_families_);
|
||||
}
|
||||
@@ -2439,6 +2515,8 @@ Status StressTest::TestFlush(const std::vector<int>& rand_column_families) {
|
||||
return db_->Flush(flush_opts, cfhs);
|
||||
}
|
||||
|
||||
Status StressTest::TestResetStats() { return db_->ResetStats(); }
|
||||
|
||||
Status StressTest::TestPauseBackground(ThreadState* thread) {
|
||||
Status status = db_->PauseBackgroundWork();
|
||||
if (!status.ok()) {
|
||||
@@ -2583,7 +2661,10 @@ void StressTest::TestCompactRange(ThreadState* thread, int64_t rand_key,
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.exclusive_manual_compaction = static_cast<bool>(thread->rand.Next() % 2);
|
||||
cro.change_level = static_cast<bool>(thread->rand.Next() % 2);
|
||||
if (static_cast<ROCKSDB_NAMESPACE::CompactionStyle>(FLAGS_compaction_style) !=
|
||||
ROCKSDB_NAMESPACE::CompactionStyle::kCompactionStyleFIFO) {
|
||||
cro.change_level = static_cast<bool>(thread->rand.Next() % 2);
|
||||
}
|
||||
if (thread->rand.OneIn(2)) {
|
||||
cro.target_level = thread->rand.Next() % options_.num_levels;
|
||||
}
|
||||
@@ -2801,7 +2882,8 @@ void StressTest::PrintEnv() const {
|
||||
fprintf(stdout, "Num times DB reopens : %d\n", FLAGS_reopen);
|
||||
fprintf(stdout, "Batches/snapshots : %d\n",
|
||||
FLAGS_test_batches_snapshots);
|
||||
fprintf(stdout, "Do update in place : %d\n", FLAGS_in_place_update);
|
||||
fprintf(stdout, "Do update in place : %d\n",
|
||||
FLAGS_inplace_update_support);
|
||||
fprintf(stdout, "Num keys per lock : %d\n",
|
||||
1 << FLAGS_log2_keys_per_lock);
|
||||
std::string compression = CompressionTypeToString(compression_type_e);
|
||||
@@ -3597,7 +3679,6 @@ void InitializeOptionsFromFlags(
|
||||
options.compression_opts.checksum = true;
|
||||
}
|
||||
options.max_manifest_file_size = FLAGS_max_manifest_file_size;
|
||||
options.inplace_update_support = FLAGS_in_place_update;
|
||||
options.max_subcompactions = static_cast<uint32_t>(FLAGS_subcompactions);
|
||||
options.allow_concurrent_memtable_write =
|
||||
FLAGS_allow_concurrent_memtable_write;
|
||||
|
||||
@@ -89,6 +89,10 @@ class StressTest {
|
||||
return {rand_key};
|
||||
}
|
||||
|
||||
virtual void TestKeyMayExist(ThreadState*, const ReadOptions&,
|
||||
const std::vector<int>&,
|
||||
const std::vector<int64_t>&) {}
|
||||
|
||||
virtual Status TestGet(ThreadState* thread, const ReadOptions& read_opts,
|
||||
const std::vector<int>& rand_column_families,
|
||||
const std::vector<int64_t>& rand_keys) = 0;
|
||||
@@ -136,6 +140,9 @@ class StressTest {
|
||||
const Slice& start_key,
|
||||
ColumnFamilyHandle* column_family);
|
||||
|
||||
virtual void TestPromoteL0(ThreadState* thread,
|
||||
ColumnFamilyHandle* column_family);
|
||||
|
||||
// Calculate a hash value for all keys in range [start_key, end_key]
|
||||
// at a certain snapshot.
|
||||
uint32_t GetRangeHash(ThreadState* thread, const Snapshot* snapshot,
|
||||
@@ -204,6 +211,8 @@ class StressTest {
|
||||
|
||||
Status TestFlush(const std::vector<int>& rand_column_families);
|
||||
|
||||
Status TestResetStats();
|
||||
|
||||
Status TestPauseBackground(ThreadState* thread);
|
||||
|
||||
Status TestDisableFileDeletions(ThreadState* thread);
|
||||
@@ -236,7 +245,8 @@ class StressTest {
|
||||
return Status::NotSupported("TestCustomOperations() must be overridden");
|
||||
}
|
||||
|
||||
void ProcessStatus(SharedState* shared, std::string msg, Status s) const;
|
||||
void ProcessStatus(SharedState* shared, std::string msg, const Status& s,
|
||||
bool ignore_injected_error = true) const;
|
||||
|
||||
void VerificationAbort(SharedState* shared, std::string msg) const;
|
||||
|
||||
|
||||
@@ -251,13 +251,14 @@ int db_stress_tool(int argc, char** argv) {
|
||||
}
|
||||
if ((FLAGS_enable_compaction_filter || FLAGS_inplace_update_support) &&
|
||||
(FLAGS_acquire_snapshot_one_in > 0 || FLAGS_compact_range_one_in > 0 ||
|
||||
FLAGS_iterpercent > 0 || FLAGS_test_batches_snapshots ||
|
||||
FLAGS_test_cf_consistency || FLAGS_check_multiget_consistency ||
|
||||
FLAGS_iterpercent > 0 || FLAGS_prefixpercent > 0 ||
|
||||
FLAGS_test_batches_snapshots || FLAGS_test_cf_consistency ||
|
||||
FLAGS_check_multiget_consistency ||
|
||||
FLAGS_check_multiget_entity_consistency)) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Error: acquire_snapshot_one_in, compact_range_one_in, iterpercent, "
|
||||
"test_batches_snapshots, test_cf_consistency, "
|
||||
"prefixpercent, test_batches_snapshots, test_cf_consistency, "
|
||||
"check_multiget_consistency, check_multiget_entity_consistency must "
|
||||
"all be 0 when using compaction filter or inplace update support\n");
|
||||
exit(1);
|
||||
|
||||
@@ -1331,14 +1331,16 @@ uint32_t MultiOpsTxnsStressTest::GenerateNextC(ThreadState* thread) {
|
||||
}
|
||||
|
||||
void MultiOpsTxnsStressTest::ProcessRecoveredPreparedTxnsHelper(
|
||||
Transaction* txn, SharedState*) {
|
||||
Transaction* txn, SharedState* shared) {
|
||||
thread_local Random rand(static_cast<uint32_t>(FLAGS_seed));
|
||||
if (rand.OneIn(2)) {
|
||||
Status s = txn->Commit();
|
||||
assert(s.ok());
|
||||
ProcessStatus(shared, "ProcessRecoveredPreparedTxnsHelper", s,
|
||||
/*ignore_injected_error=*/false);
|
||||
} else {
|
||||
Status s = txn->Rollback();
|
||||
assert(s.ok());
|
||||
ProcessStatus(shared, "ProcessRecoveredPreparedTxnsHelper", s,
|
||||
/*ignore_injected_error=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1517,14 +1519,15 @@ void MultiOpsTxnsStressTest::PreloadDb(SharedState* shared, int threads,
|
||||
WriteBatch wb;
|
||||
const auto primary_index_entry = record.EncodePrimaryIndexEntry();
|
||||
Status s = wb.Put(primary_index_entry.first, primary_index_entry.second);
|
||||
assert(s.ok());
|
||||
ProcessStatus(shared, "PreloadDB", s, /*ignore_injected_error=*/false);
|
||||
|
||||
const auto secondary_index_entry = record.EncodeSecondaryIndexEntry();
|
||||
s = wb.Put(secondary_index_entry.first, secondary_index_entry.second);
|
||||
assert(s.ok());
|
||||
ProcessStatus(shared, "PreloadDB", s, /*ignore_injected_error=*/false);
|
||||
|
||||
s = txn_db_->Write(wopts, &wb);
|
||||
assert(s.ok());
|
||||
ProcessStatus(shared, "PreloadDB", s, /*ignore_injected_error=*/false);
|
||||
|
||||
// TODO (yanqin): make the following check optional, especially when data
|
||||
// size is large.
|
||||
@@ -1532,7 +1535,7 @@ void MultiOpsTxnsStressTest::PreloadDb(SharedState* shared, int threads,
|
||||
tmp_rec.SetB(record.b_value());
|
||||
s = tmp_rec.DecodeSecondaryIndexEntry(secondary_index_entry.first,
|
||||
secondary_index_entry.second);
|
||||
assert(s.ok());
|
||||
ProcessStatus(shared, "PreloadDB", s, /*ignore_injected_error=*/false);
|
||||
assert(tmp_rec == record);
|
||||
|
||||
existing_a_uniqs[tid].insert(a);
|
||||
|
||||
@@ -462,6 +462,44 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
|
||||
bool IsStateTracked() const override { return true; }
|
||||
|
||||
void TestKeyMayExist(ThreadState* thread, const ReadOptions& read_opts,
|
||||
const std::vector<int>& rand_column_families,
|
||||
const std::vector<int64_t>& rand_keys) override {
|
||||
auto cfh = column_families_[rand_column_families[0]];
|
||||
std::string key_str = Key(rand_keys[0]);
|
||||
Slice key = key_str;
|
||||
std::string ignore;
|
||||
ReadOptions read_opts_copy = read_opts;
|
||||
|
||||
std::string read_ts_str;
|
||||
Slice read_ts_slice;
|
||||
if (FLAGS_user_timestamp_size > 0) {
|
||||
read_ts_str = GetNowNanos();
|
||||
read_ts_slice = read_ts_str;
|
||||
read_opts_copy.timestamp = &read_ts_slice;
|
||||
}
|
||||
bool read_older_ts = MaybeUseOlderTimestampForPointLookup(
|
||||
thread, read_ts_str, read_ts_slice, read_opts_copy);
|
||||
|
||||
const ExpectedValue pre_read_expected_value =
|
||||
thread->shared->Get(rand_column_families[0], rand_keys[0]);
|
||||
bool key_may_exist = db_->KeyMayExist(read_opts_copy, cfh, key, &ignore);
|
||||
const ExpectedValue post_read_expected_value =
|
||||
thread->shared->Get(rand_column_families[0], rand_keys[0]);
|
||||
|
||||
if (!key_may_exist && !FLAGS_skip_verifydb && !read_older_ts) {
|
||||
if (ExpectedValueHelper::MustHaveExisted(pre_read_expected_value,
|
||||
post_read_expected_value)) {
|
||||
thread->shared->SetVerificationFailure();
|
||||
fprintf(stderr,
|
||||
"error : inconsistent values for key %s: expected state has "
|
||||
"the key, TestKeyMayExist() returns false indicating the key "
|
||||
"must not exist.\n",
|
||||
key.ToString(true).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status TestGet(ThreadState* thread, const ReadOptions& read_opts,
|
||||
const std::vector<int>& rand_column_families,
|
||||
const std::vector<int64_t>& rand_keys) override {
|
||||
@@ -916,7 +954,8 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
|
||||
const std::string key = Key(rand_keys[0]);
|
||||
|
||||
PinnableWideColumns from_db;
|
||||
PinnableWideColumns columns_from_db;
|
||||
PinnableAttributeGroups attribute_groups_from_db;
|
||||
|
||||
ReadOptions read_opts_copy = read_opts;
|
||||
std::string read_ts_str;
|
||||
@@ -929,7 +968,16 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
bool read_older_ts = MaybeUseOlderTimestampForPointLookup(
|
||||
thread, read_ts_str, read_ts_slice, read_opts_copy);
|
||||
|
||||
const Status s = db_->GetEntity(read_opts_copy, cfh, key, &from_db);
|
||||
Status s;
|
||||
if (FLAGS_use_attribute_group) {
|
||||
attribute_groups_from_db.emplace_back(cfh);
|
||||
s = db_->GetEntity(read_opts_copy, key, &attribute_groups_from_db);
|
||||
if (s.ok()) {
|
||||
s = attribute_groups_from_db.back().status();
|
||||
}
|
||||
} else {
|
||||
s = db_->GetEntity(read_opts_copy, cfh, key, &columns_from_db);
|
||||
}
|
||||
|
||||
int error_count = 0;
|
||||
|
||||
@@ -953,7 +1001,13 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
thread->stats.AddGets(1, 1);
|
||||
|
||||
if (!FLAGS_skip_verifydb && !read_older_ts) {
|
||||
const WideColumns& columns = from_db.columns();
|
||||
if (FLAGS_use_attribute_group) {
|
||||
assert(!attribute_groups_from_db.empty());
|
||||
}
|
||||
const WideColumns& columns =
|
||||
FLAGS_use_attribute_group
|
||||
? attribute_groups_from_db.back().columns()
|
||||
: columns_from_db.columns();
|
||||
ExpectedValue expected =
|
||||
shared->Get(rand_column_families[0], rand_keys[0]);
|
||||
if (!VerifyWideColumns(columns)) {
|
||||
@@ -1023,130 +1077,257 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
std::vector<std::string> keys(num_keys);
|
||||
std::vector<Slice> key_slices(num_keys);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
keys[i] = Key(rand_keys[i]);
|
||||
key_slices[i] = keys[i];
|
||||
}
|
||||
|
||||
std::vector<PinnableWideColumns> results(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
|
||||
if (fault_fs_guard) {
|
||||
fault_fs_guard->EnableErrorInjection();
|
||||
SharedState::ignore_read_error = false;
|
||||
}
|
||||
|
||||
db_->MultiGetEntity(read_opts_copy, cfh, num_keys, key_slices.data(),
|
||||
results.data(), statuses.data());
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
keys[i] = Key(rand_keys[i]);
|
||||
key_slices[i] = keys[i];
|
||||
}
|
||||
|
||||
int error_count = 0;
|
||||
|
||||
if (fault_fs_guard) {
|
||||
error_count = fault_fs_guard->GetAndResetErrorCount();
|
||||
|
||||
if (error_count && !SharedState::ignore_read_error) {
|
||||
int stat_nok = 0;
|
||||
for (const auto& s : statuses) {
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
stat_nok++;
|
||||
}
|
||||
}
|
||||
|
||||
if (stat_nok < error_count) {
|
||||
// Grab mutex so multiple threads don't try to print the
|
||||
// stack trace at the same time
|
||||
assert(thread->shared);
|
||||
MutexLock l(thread->shared->GetMutex());
|
||||
|
||||
fprintf(stderr, "Didn't get expected error from MultiGetEntity\n");
|
||||
fprintf(stderr, "num_keys %zu Expected %d errors, seen %d\n",
|
||||
num_keys, error_count, stat_nok);
|
||||
fprintf(stderr, "Call stack that injected the fault\n");
|
||||
fault_fs_guard->PrintFaultBacktrace();
|
||||
std::terminate();
|
||||
auto handle_result = [](ThreadState* _thread, const Status& s,
|
||||
bool is_consistent, int err_count) {
|
||||
if (!is_consistent) {
|
||||
fprintf(stderr,
|
||||
"TestMultiGetEntity%s error: results are not consistent\n",
|
||||
FLAGS_use_attribute_group ? "(AttributeGroup)" : "");
|
||||
_thread->stats.AddErrors(1);
|
||||
// Fail fast to preserve the DB state
|
||||
_thread->shared->SetVerificationFailure();
|
||||
} else if (s.ok()) {
|
||||
_thread->stats.AddGets(1, 1);
|
||||
} else if (s.IsNotFound()) {
|
||||
_thread->stats.AddGets(1, 0);
|
||||
} else {
|
||||
if (err_count == 0) {
|
||||
fprintf(stderr, "MultiGetEntity%s error: %s\n",
|
||||
FLAGS_use_attribute_group ? "(AttributeGroup)" : "",
|
||||
s.ToString().c_str());
|
||||
_thread->stats.AddErrors(1);
|
||||
} else {
|
||||
_thread->stats.AddVerifiedErrors(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fault_fs_guard->DisableErrorInjection();
|
||||
}
|
||||
if (FLAGS_use_attribute_group) {
|
||||
// AttributeGroup MultiGetEntity verification
|
||||
|
||||
const bool check_get_entity =
|
||||
!error_count && FLAGS_check_multiget_entity_consistency;
|
||||
std::vector<PinnableAttributeGroups> results;
|
||||
results.reserve(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
PinnableAttributeGroups attribute_groups;
|
||||
attribute_groups.emplace_back(cfh);
|
||||
results.emplace_back(std::move(attribute_groups));
|
||||
}
|
||||
db_->MultiGetEntity(read_opts_copy, num_keys, key_slices.data(),
|
||||
results.data());
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Status& s = statuses[i];
|
||||
if (fault_fs_guard) {
|
||||
error_count = fault_fs_guard->GetAndResetErrorCount();
|
||||
|
||||
bool is_consistent = true;
|
||||
|
||||
if (s.ok() && !VerifyWideColumns(results[i].columns())) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"error : inconsistent columns returned by MultiGetEntity for key "
|
||||
"%s: %s\n",
|
||||
StringToHex(keys[i]).c_str(),
|
||||
WideColumnsToHex(results[i].columns()).c_str());
|
||||
is_consistent = false;
|
||||
} else if (check_get_entity && (s.ok() || s.IsNotFound())) {
|
||||
PinnableWideColumns cmp_result;
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_GETENTITY);
|
||||
const Status cmp_s =
|
||||
db_->GetEntity(read_opts_copy, cfh, key_slices[i], &cmp_result);
|
||||
|
||||
if (!cmp_s.ok() && !cmp_s.IsNotFound()) {
|
||||
fprintf(stderr, "GetEntity error: %s\n", cmp_s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
} else if (cmp_s.IsNotFound()) {
|
||||
if (s.ok()) {
|
||||
fprintf(stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity returned "
|
||||
"ok, GetEntity returned not found\n",
|
||||
StringToHex(keys[i]).c_str());
|
||||
is_consistent = false;
|
||||
if (error_count && !SharedState::ignore_read_error) {
|
||||
int stat_nok = 0;
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Status& s = results[i][0].status();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
stat_nok++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert(cmp_s.ok());
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
if (stat_nok < error_count) {
|
||||
// Grab mutex so multiple threads don't try to print the
|
||||
// stack trace at the same time
|
||||
assert(thread->shared);
|
||||
MutexLock l(thread->shared->GetMutex());
|
||||
|
||||
fprintf(stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity returned "
|
||||
"not found, GetEntity returned ok\n",
|
||||
StringToHex(keys[i]).c_str());
|
||||
is_consistent = false;
|
||||
} else {
|
||||
assert(s.ok());
|
||||
"Didn't get expected error from MultiGetEntity "
|
||||
"(AttributeGroup)\n");
|
||||
fprintf(stderr, "num_keys %zu Expected %d errors, seen %d\n",
|
||||
num_keys, error_count, stat_nok);
|
||||
fprintf(stderr, "Call stack that injected the fault\n");
|
||||
fault_fs_guard->PrintFaultBacktrace();
|
||||
std::terminate();
|
||||
}
|
||||
}
|
||||
fault_fs_guard->DisableErrorInjection();
|
||||
}
|
||||
|
||||
if (results[i] != cmp_result) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity returned "
|
||||
"%s, GetEntity returned %s\n",
|
||||
// Compare against non-attribute-group GetEntity result
|
||||
const bool check_get_entity =
|
||||
!error_count && FLAGS_check_multiget_entity_consistency;
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
assert(results[i].size() == 1);
|
||||
const Status& s = results[i][0].status();
|
||||
|
||||
bool is_consistent = true;
|
||||
|
||||
if (s.ok() && !VerifyWideColumns(results[i][0].columns())) {
|
||||
fprintf(stderr,
|
||||
"error : inconsistent columns returned by MultiGetEntity "
|
||||
"(AttributeGroup) for key "
|
||||
"%s: %s\n",
|
||||
StringToHex(keys[i]).c_str(),
|
||||
WideColumnsToHex(results[i].columns()).c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str());
|
||||
WideColumnsToHex(results[i][0].columns()).c_str());
|
||||
is_consistent = false;
|
||||
} else if (check_get_entity && (s.ok() || s.IsNotFound())) {
|
||||
PinnableWideColumns cmp_result;
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_GETENTITY);
|
||||
const Status cmp_s =
|
||||
db_->GetEntity(read_opts_copy, cfh, key_slices[i], &cmp_result);
|
||||
|
||||
if (!cmp_s.ok() && !cmp_s.IsNotFound()) {
|
||||
fprintf(stderr, "GetEntity error: %s\n", cmp_s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
} else if (cmp_s.IsNotFound()) {
|
||||
if (s.ok()) {
|
||||
fprintf(stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity "
|
||||
"(AttributeGroup) returned "
|
||||
"ok, GetEntity returned not found\n",
|
||||
StringToHex(keys[i]).c_str());
|
||||
is_consistent = false;
|
||||
}
|
||||
} else {
|
||||
assert(cmp_s.ok());
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
fprintf(stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity "
|
||||
"(AttributeGroup) returned "
|
||||
"not found, GetEntity returned ok\n",
|
||||
StringToHex(keys[i]).c_str());
|
||||
is_consistent = false;
|
||||
} else {
|
||||
assert(s.ok());
|
||||
|
||||
if (results[i][0].columns() != cmp_result.columns()) {
|
||||
fprintf(stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity "
|
||||
"(AttributeGroup) returned "
|
||||
"%s, GetEntity returned %s\n",
|
||||
StringToHex(keys[i]).c_str(),
|
||||
WideColumnsToHex(results[i][0].columns()).c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str());
|
||||
is_consistent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handle_result(thread, s, is_consistent, error_count);
|
||||
if (!is_consistent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
std::vector<PinnableWideColumns> results(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
|
||||
db_->MultiGetEntity(read_opts_copy, cfh, num_keys, key_slices.data(),
|
||||
results.data(), statuses.data());
|
||||
|
||||
if (fault_fs_guard) {
|
||||
error_count = fault_fs_guard->GetAndResetErrorCount();
|
||||
|
||||
if (error_count && !SharedState::ignore_read_error) {
|
||||
int stat_nok = 0;
|
||||
for (const auto& s : statuses) {
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
stat_nok++;
|
||||
}
|
||||
}
|
||||
|
||||
if (stat_nok < error_count) {
|
||||
// Grab mutex so multiple threads don't try to print the
|
||||
// stack trace at the same time
|
||||
assert(thread->shared);
|
||||
MutexLock l(thread->shared->GetMutex());
|
||||
|
||||
fprintf(stderr, "Didn't get expected error from MultiGetEntity\n");
|
||||
fprintf(stderr, "num_keys %zu Expected %d errors, seen %d\n",
|
||||
num_keys, error_count, stat_nok);
|
||||
fprintf(stderr, "Call stack that injected the fault\n");
|
||||
fault_fs_guard->PrintFaultBacktrace();
|
||||
std::terminate();
|
||||
}
|
||||
}
|
||||
|
||||
fault_fs_guard->DisableErrorInjection();
|
||||
}
|
||||
|
||||
if (!is_consistent) {
|
||||
fprintf(stderr,
|
||||
"TestMultiGetEntity error: results are not consistent\n");
|
||||
thread->stats.AddErrors(1);
|
||||
// Fail fast to preserve the DB state
|
||||
thread->shared->SetVerificationFailure();
|
||||
break;
|
||||
} else if (s.ok()) {
|
||||
thread->stats.AddGets(1, 1);
|
||||
} else if (s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
} else {
|
||||
if (error_count == 0) {
|
||||
fprintf(stderr, "MultiGetEntity error: %s\n", s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
} else {
|
||||
thread->stats.AddVerifiedErrors(1);
|
||||
const bool check_get_entity =
|
||||
!error_count && FLAGS_check_multiget_entity_consistency;
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Status& s = statuses[i];
|
||||
|
||||
bool is_consistent = true;
|
||||
|
||||
if (s.ok() && !VerifyWideColumns(results[i].columns())) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"error : inconsistent columns returned by MultiGetEntity for key "
|
||||
"%s: %s\n",
|
||||
StringToHex(keys[i]).c_str(),
|
||||
WideColumnsToHex(results[i].columns()).c_str());
|
||||
is_consistent = false;
|
||||
} else if (check_get_entity && (s.ok() || s.IsNotFound())) {
|
||||
PinnableWideColumns cmp_result;
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_GETENTITY);
|
||||
const Status cmp_s =
|
||||
db_->GetEntity(read_opts_copy, cfh, key_slices[i], &cmp_result);
|
||||
|
||||
if (!cmp_s.ok() && !cmp_s.IsNotFound()) {
|
||||
fprintf(stderr, "GetEntity error: %s\n", cmp_s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
} else if (cmp_s.IsNotFound()) {
|
||||
if (s.ok()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity returned "
|
||||
"ok, GetEntity returned not found\n",
|
||||
StringToHex(keys[i]).c_str());
|
||||
is_consistent = false;
|
||||
}
|
||||
} else {
|
||||
assert(cmp_s.ok());
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity returned "
|
||||
"not found, GetEntity returned ok\n",
|
||||
StringToHex(keys[i]).c_str());
|
||||
is_consistent = false;
|
||||
} else {
|
||||
assert(s.ok());
|
||||
|
||||
if (results[i] != cmp_result) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Inconsistent results for key %s: MultiGetEntity returned "
|
||||
"%s, GetEntity returned %s\n",
|
||||
StringToHex(keys[i]).c_str(),
|
||||
WideColumnsToHex(results[i].columns()).c_str(),
|
||||
WideColumnsToHex(cmp_result.columns()).c_str());
|
||||
is_consistent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handle_result(thread, s, is_consistent, error_count);
|
||||
if (!is_consistent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1302,8 +1483,13 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
|
||||
if (FLAGS_use_put_entity_one_in > 0 &&
|
||||
(value_base % FLAGS_use_put_entity_one_in) == 0) {
|
||||
s = db_->PutEntity(write_opts, cfh, k,
|
||||
GenerateWideColumns(value_base, v));
|
||||
if (FLAGS_use_attribute_group) {
|
||||
s = db_->PutEntity(write_opts, k,
|
||||
GenerateAttributeGroups({cfh}, value_base, v));
|
||||
} else {
|
||||
s = db_->PutEntity(write_opts, cfh, k,
|
||||
GenerateWideColumns(value_base, v));
|
||||
}
|
||||
} else if (FLAGS_use_timed_put_one_in > 0 &&
|
||||
((value_base + kLargePrimeForCommonFactorSkew) %
|
||||
FLAGS_use_timed_put_one_in) == 0) {
|
||||
@@ -2079,7 +2265,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
bool VerifyOrSyncValue(int cf, int64_t key, const ReadOptions& /*opts*/,
|
||||
bool VerifyOrSyncValue(int cf, int64_t key, const ReadOptions& opts,
|
||||
SharedState* shared, const std::string& value_from_db,
|
||||
std::string msg_prefix, const Status& s) const {
|
||||
if (shared->HasVerificationFailedYet()) {
|
||||
@@ -2105,27 +2291,43 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
GenerateValue(expected_value.GetValueBase(), expected_value_data,
|
||||
sizeof(expected_value_data));
|
||||
|
||||
std::ostringstream read_u64ts;
|
||||
if (opts.timestamp) {
|
||||
read_u64ts << " while read with timestamp: ";
|
||||
uint64_t read_ts;
|
||||
if (DecodeU64Ts(*opts.timestamp, &read_ts).ok()) {
|
||||
read_u64ts << std::to_string(read_ts) << ", ";
|
||||
} else {
|
||||
read_u64ts << s.ToString()
|
||||
<< " Encoded read timestamp: " << opts.timestamp->ToString()
|
||||
<< ", ";
|
||||
}
|
||||
}
|
||||
|
||||
// compare value_from_db with the value in the shared state
|
||||
if (s.ok()) {
|
||||
const Slice slice(value_from_db);
|
||||
const uint32_t value_base_from_db = GetValueBase(slice);
|
||||
if (ExpectedValueHelper::MustHaveNotExisted(expected_value,
|
||||
expected_value)) {
|
||||
VerificationAbort(shared, msg_prefix + ": Unexpected value found", cf,
|
||||
key, value_from_db, "");
|
||||
VerificationAbort(
|
||||
shared, msg_prefix + ": Unexpected value found" + read_u64ts.str(),
|
||||
cf, key, value_from_db, "");
|
||||
return false;
|
||||
}
|
||||
if (!ExpectedValueHelper::InExpectedValueBaseRange(
|
||||
value_base_from_db, expected_value, expected_value)) {
|
||||
VerificationAbort(shared, msg_prefix + ": Unexpected value found", cf,
|
||||
key, value_from_db,
|
||||
Slice(expected_value_data, expected_value_data_size));
|
||||
VerificationAbort(
|
||||
shared, msg_prefix + ": Unexpected value found" + read_u64ts.str(),
|
||||
cf, key, value_from_db,
|
||||
Slice(expected_value_data, expected_value_data_size));
|
||||
return false;
|
||||
}
|
||||
// TODO: are the length/memcmp() checks repetitive?
|
||||
if (value_from_db.length() != expected_value_data_size) {
|
||||
VerificationAbort(shared,
|
||||
msg_prefix + ": Length of value read is not equal",
|
||||
msg_prefix + ": Length of value read is not equal" +
|
||||
read_u64ts.str(),
|
||||
cf, key, value_from_db,
|
||||
Slice(expected_value_data, expected_value_data_size));
|
||||
return false;
|
||||
@@ -2133,7 +2335,8 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
if (memcmp(value_from_db.data(), expected_value_data,
|
||||
expected_value_data_size) != 0) {
|
||||
VerificationAbort(shared,
|
||||
msg_prefix + ": Contents of value read don't match",
|
||||
msg_prefix + ": Contents of value read don't match" +
|
||||
read_u64ts.str(),
|
||||
cf, key, value_from_db,
|
||||
Slice(expected_value_data, expected_value_data_size));
|
||||
return false;
|
||||
@@ -2142,14 +2345,16 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
if (ExpectedValueHelper::MustHaveExisted(expected_value,
|
||||
expected_value)) {
|
||||
VerificationAbort(
|
||||
shared, msg_prefix + ": Value not found: " + s.ToString(), cf, key,
|
||||
"", Slice(expected_value_data, expected_value_data_size));
|
||||
shared,
|
||||
msg_prefix + ": Value not found " + read_u64ts.str() + s.ToString(),
|
||||
cf, key, "", Slice(expected_value_data, expected_value_data_size));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
VerificationAbort(shared, msg_prefix + "Non-OK status: " + s.ToString(),
|
||||
cf, key, "",
|
||||
Slice(expected_value_data, expected_value_data_size));
|
||||
VerificationAbort(
|
||||
shared,
|
||||
msg_prefix + "Non-OK status " + read_u64ts.str() + s.ToString(), cf,
|
||||
key, "", Slice(expected_value_data, expected_value_data_size));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
Vendored
+1
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "env/fs_on_demand.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
|
||||
#include "file/filename.h"
|
||||
|
||||
Vendored
+3
-1
@@ -1441,10 +1441,12 @@ void PosixWritableFile::SetWriteLifeTimeHint(Env::WriteLifeTimeHint hint) {
|
||||
#ifdef OS_LINUX
|
||||
// Suppress Valgrind "Unimplemented functionality" error.
|
||||
#ifndef ROCKSDB_VALGRIND_RUN
|
||||
uint64_t fcntl_hint = hint;
|
||||
|
||||
if (hint == write_hint_) {
|
||||
return;
|
||||
}
|
||||
if (fcntl(fd_, F_SET_RW_HINT, &hint) == 0) {
|
||||
if (fcntl(fd_, F_SET_RW_HINT, &fcntl_hint) == 0) {
|
||||
write_hint_ = hint;
|
||||
}
|
||||
#else
|
||||
|
||||
+124
-23
@@ -31,6 +31,7 @@ DeleteScheduler::DeleteScheduler(SystemClock* clock, FileSystem* fs,
|
||||
total_trash_size_(0),
|
||||
rate_bytes_per_sec_(rate_bytes_per_sec),
|
||||
pending_files_(0),
|
||||
next_trash_bucket_(0),
|
||||
bytes_max_delete_chunk_(bytes_max_delete_chunk),
|
||||
closing_(false),
|
||||
cv_(&mu_),
|
||||
@@ -66,10 +67,8 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
total_trash_size_.load() > total_size * max_trash_db_ratio_.load())) {
|
||||
// Rate limiting is disabled or trash size makes up more than
|
||||
// max_trash_db_ratio_ (default 25%) of the total DB size
|
||||
TEST_SYNC_POINT("DeleteScheduler::DeleteFile");
|
||||
Status s = fs_->DeleteFile(file_path, IOOptions(), nullptr);
|
||||
Status s = DeleteFileImmediately(file_path, /*accounted=*/true);
|
||||
if (s.ok()) {
|
||||
s = sst_file_manager_->OnDeleteFile(file_path);
|
||||
ROCKS_LOG_INFO(info_log_,
|
||||
"Deleted file %s immediately, rate_bytes_per_sec %" PRIi64
|
||||
", total_trash_size %" PRIu64 ", total_size %" PRIi64
|
||||
@@ -77,15 +76,57 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
file_path.c_str(), rate_bytes_per_sec_.load(),
|
||||
total_trash_size_.load(), total_size,
|
||||
max_trash_db_ratio_.load());
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
RecordTick(stats_.get(), FILES_DELETED_IMMEDIATELY);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
return AddFileToDeletionQueue(file_path, dir_to_sync, /*bucket=*/std::nullopt,
|
||||
/*accounted=*/true);
|
||||
}
|
||||
|
||||
Status DeleteScheduler::DeleteUnaccountedFile(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg,
|
||||
std::optional<int32_t> bucket) {
|
||||
uint64_t num_hard_links = 1;
|
||||
fs_->NumFileLinks(file_path, IOOptions(), &num_hard_links, nullptr)
|
||||
.PermitUncheckedError();
|
||||
|
||||
// We can tolerate rare races where we might immediately delete both links
|
||||
// to a file.
|
||||
if (rate_bytes_per_sec_.load() <= 0 || (!force_bg && num_hard_links > 1)) {
|
||||
Status s = DeleteFileImmediately(file_path, /*accounted=*/false);
|
||||
if (s.ok()) {
|
||||
ROCKS_LOG_INFO(info_log_,
|
||||
"Deleted file %s immediately, rate_bytes_per_sec %" PRIi64,
|
||||
file_path.c_str(), rate_bytes_per_sec_.load());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
return AddFileToDeletionQueue(file_path, dir_to_sync, bucket,
|
||||
/*accounted=*/false);
|
||||
}
|
||||
|
||||
Status DeleteScheduler::DeleteFileImmediately(const std::string& file_path,
|
||||
bool accounted) {
|
||||
TEST_SYNC_POINT("DeleteScheduler::DeleteFile");
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::DeleteFile::cb",
|
||||
const_cast<std::string*>(&file_path));
|
||||
Status s = fs_->DeleteFile(file_path, IOOptions(), nullptr);
|
||||
if (s.ok()) {
|
||||
s = OnDeleteFile(file_path, accounted);
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
RecordTick(stats_.get(), FILES_DELETED_IMMEDIATELY);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DeleteScheduler::AddFileToDeletionQueue(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
std::optional<int32_t> bucket,
|
||||
bool accounted) {
|
||||
// Move file to trash
|
||||
std::string trash_file;
|
||||
Status s = MarkAsTrash(file_path, &trash_file);
|
||||
Status s = MarkAsTrash(file_path, accounted, &trash_file);
|
||||
ROCKS_LOG_INFO(info_log_, "Mark file: %s as trash -- %s", trash_file.c_str(),
|
||||
s.ToString().c_str());
|
||||
|
||||
@@ -94,7 +135,7 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
file_path.c_str(), s.ToString().c_str());
|
||||
s = fs_->DeleteFile(file_path, IOOptions(), nullptr);
|
||||
if (s.ok()) {
|
||||
s = sst_file_manager_->OnDeleteFile(file_path);
|
||||
s = OnDeleteFile(file_path, accounted);
|
||||
ROCKS_LOG_INFO(info_log_, "Deleted file %s immediately",
|
||||
trash_file.c_str());
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
@@ -104,11 +145,13 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
}
|
||||
|
||||
// Update the total trash size
|
||||
uint64_t trash_file_size = 0;
|
||||
IOStatus io_s =
|
||||
fs_->GetFileSize(trash_file, IOOptions(), &trash_file_size, nullptr);
|
||||
if (io_s.ok()) {
|
||||
total_trash_size_.fetch_add(trash_file_size);
|
||||
if (accounted) {
|
||||
uint64_t trash_file_size = 0;
|
||||
IOStatus io_s =
|
||||
fs_->GetFileSize(trash_file, IOOptions(), &trash_file_size, nullptr);
|
||||
if (io_s.ok()) {
|
||||
total_trash_size_.fetch_add(trash_file_size);
|
||||
}
|
||||
}
|
||||
//**TODO: What should we do if we failed to
|
||||
// get the file size?
|
||||
@@ -117,8 +160,15 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
{
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
RecordTick(stats_.get(), FILES_MARKED_TRASH);
|
||||
queue_.emplace(trash_file, dir_to_sync);
|
||||
queue_.emplace(trash_file, dir_to_sync, accounted, bucket);
|
||||
pending_files_++;
|
||||
if (bucket.has_value()) {
|
||||
auto iter = pending_files_in_buckets_.find(bucket.value());
|
||||
assert(iter != pending_files_in_buckets_.end());
|
||||
if (iter != pending_files_in_buckets_.end()) {
|
||||
iter->second++;
|
||||
}
|
||||
}
|
||||
if (pending_files_ == 1) {
|
||||
cv_.SignalAll();
|
||||
}
|
||||
@@ -177,7 +227,7 @@ Status DeleteScheduler::CleanupDirectory(Env* env, SstFileManagerImpl* sfm,
|
||||
}
|
||||
|
||||
Status DeleteScheduler::MarkAsTrash(const std::string& file_path,
|
||||
std::string* trash_file) {
|
||||
bool accounted, std::string* trash_file) {
|
||||
// Sanity check of the path
|
||||
size_t idx = file_path.rfind('/');
|
||||
if (idx == std::string::npos || idx == file_path.size() - 1) {
|
||||
@@ -211,7 +261,7 @@ Status DeleteScheduler::MarkAsTrash(const std::string& file_path,
|
||||
}
|
||||
cnt++;
|
||||
}
|
||||
if (s.ok()) {
|
||||
if (s.ok() && accounted) {
|
||||
s = sst_file_manager_->OnMoveFile(file_path, *trash_file);
|
||||
}
|
||||
return s;
|
||||
@@ -235,6 +285,8 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
uint64_t total_deleted_bytes = 0;
|
||||
int64_t current_delete_rate = rate_bytes_per_sec_.load();
|
||||
while (!queue_.empty() && !closing_) {
|
||||
// Satisfy static analysis.
|
||||
std::optional<int32_t> bucket = std::nullopt;
|
||||
if (current_delete_rate != rate_bytes_per_sec_.load()) {
|
||||
// User changed the delete rate
|
||||
current_delete_rate = rate_bytes_per_sec_.load();
|
||||
@@ -247,14 +299,17 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
// Get new file to delete
|
||||
const FileAndDir& fad = queue_.front();
|
||||
std::string path_in_trash = fad.fname;
|
||||
std::string dir_to_sync = fad.dir;
|
||||
bool accounted = fad.accounted;
|
||||
bucket = fad.bucket;
|
||||
|
||||
// We don't need to hold the lock while deleting the file
|
||||
mu_.Unlock();
|
||||
uint64_t deleted_bytes = 0;
|
||||
bool is_complete = true;
|
||||
// Delete file from trash and update total_penlty value
|
||||
Status s =
|
||||
DeleteTrashFile(path_in_trash, fad.dir, &deleted_bytes, &is_complete);
|
||||
Status s = DeleteTrashFile(path_in_trash, dir_to_sync, accounted,
|
||||
&deleted_bytes, &is_complete);
|
||||
total_deleted_bytes += deleted_bytes;
|
||||
mu_.Lock();
|
||||
if (is_complete) {
|
||||
@@ -288,12 +343,20 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::BackgroundEmptyTrash:Wait",
|
||||
&total_penalty);
|
||||
|
||||
int32_t pending_files_in_bucket = std::numeric_limits<int32_t>::max();
|
||||
if (is_complete) {
|
||||
pending_files_--;
|
||||
if (bucket.has_value()) {
|
||||
auto iter = pending_files_in_buckets_.find(bucket.value());
|
||||
assert(iter != pending_files_in_buckets_.end());
|
||||
if (iter != pending_files_in_buckets_.end()) {
|
||||
pending_files_in_bucket = iter->second--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pending_files_ == 0) {
|
||||
// Unblock WaitForEmptyTrash since there are no more files waiting
|
||||
// to be deleted
|
||||
if (pending_files_ == 0 || pending_files_in_bucket == 0) {
|
||||
// Unblock WaitForEmptyTrash or WaitForEmptyTrashBucket since there are
|
||||
// no more files waiting to be deleted
|
||||
cv_.SignalAll();
|
||||
}
|
||||
}
|
||||
@@ -302,12 +365,14 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
|
||||
Status DeleteScheduler::DeleteTrashFile(const std::string& path_in_trash,
|
||||
const std::string& dir_to_sync,
|
||||
uint64_t* deleted_bytes,
|
||||
bool accounted, uint64_t* deleted_bytes,
|
||||
bool* is_complete) {
|
||||
uint64_t file_size;
|
||||
Status s = fs_->GetFileSize(path_in_trash, IOOptions(), &file_size, nullptr);
|
||||
*is_complete = true;
|
||||
TEST_SYNC_POINT("DeleteScheduler::DeleteTrashFile:DeleteFile");
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::DeleteTrashFile::cb",
|
||||
const_cast<std::string*>(&path_in_trash));
|
||||
if (s.ok()) {
|
||||
bool need_full_delete = true;
|
||||
if (bytes_max_delete_chunk_ != 0 && file_size > bytes_max_delete_chunk_) {
|
||||
@@ -374,7 +439,7 @@ Status DeleteScheduler::DeleteTrashFile(const std::string& path_in_trash,
|
||||
}
|
||||
if (s.ok()) {
|
||||
*deleted_bytes = file_size;
|
||||
s = sst_file_manager_->OnDeleteFile(path_in_trash);
|
||||
s = OnDeleteFile(path_in_trash, accounted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,12 +449,24 @@ Status DeleteScheduler::DeleteTrashFile(const std::string& path_in_trash,
|
||||
path_in_trash.c_str(), s.ToString().c_str());
|
||||
*deleted_bytes = 0;
|
||||
} else {
|
||||
total_trash_size_.fetch_sub(*deleted_bytes);
|
||||
if (accounted) {
|
||||
total_trash_size_.fetch_sub(*deleted_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DeleteScheduler::OnDeleteFile(const std::string& file_path,
|
||||
bool accounted) {
|
||||
if (accounted) {
|
||||
return sst_file_manager_->OnDeleteFile(file_path);
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::OnDeleteFile",
|
||||
const_cast<std::string*>(&file_path));
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void DeleteScheduler::WaitForEmptyTrash() {
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
while (pending_files_ > 0 && !closing_) {
|
||||
@@ -397,6 +474,30 @@ void DeleteScheduler::WaitForEmptyTrash() {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int32_t> DeleteScheduler::NewTrashBucket() {
|
||||
if (rate_bytes_per_sec_.load() <= 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
int32_t bucket_number = next_trash_bucket_++;
|
||||
pending_files_in_buckets_.emplace(bucket_number, 0);
|
||||
return bucket_number;
|
||||
}
|
||||
|
||||
void DeleteScheduler::WaitForEmptyTrashBucket(int32_t bucket) {
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
if (bucket >= next_trash_bucket_) {
|
||||
return;
|
||||
}
|
||||
auto iter = pending_files_in_buckets_.find(bucket);
|
||||
while (iter != pending_files_in_buckets_.end() && iter->second > 0 &&
|
||||
!closing_) {
|
||||
cv_.Wait();
|
||||
iter = pending_files_in_buckets_.find(bucket);
|
||||
}
|
||||
pending_files_in_buckets_.erase(bucket);
|
||||
}
|
||||
|
||||
void DeleteScheduler::MaybeCreateBackgroundThread() {
|
||||
if (bg_thread_ == nullptr && rate_bytes_per_sec_.load() > 0) {
|
||||
bg_thread_.reset(
|
||||
|
||||
+61
-8
@@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -48,16 +49,45 @@ class DeleteScheduler {
|
||||
MaybeCreateBackgroundThread();
|
||||
}
|
||||
|
||||
// Mark file as trash directory and schedule its deletion. If force_bg is
|
||||
// set, it forces the file to always be deleted in the background thread,
|
||||
// except when rate limiting is disabled
|
||||
// Delete an accounted file that is tracked by `SstFileManager` and should be
|
||||
// tracked by this `DeleteScheduler` when it's deleted.
|
||||
// The file is deleted immediately if slow deletion is disabled. If force_bg
|
||||
// is not set and trash to db size ratio exceeded the configured threshold,
|
||||
// it is immediately deleted too. In all other cases, the file will be moved
|
||||
// to a trash directory and scheduled for deletion by a background thread.
|
||||
Status DeleteFile(const std::string& fname, const std::string& dir_to_sync,
|
||||
const bool force_bg = false);
|
||||
|
||||
// Wait for all files being deleteing in the background to finish or for
|
||||
// Delete an unaccounted file that is not tracked by `SstFileManager` and
|
||||
// should not be tracked by this `DeleteScheduler` when it's deleted.
|
||||
// The file is deleted immediately if slow deletion is disabled. If force_bg
|
||||
// is not set and the file have more than 1 hard link, it is immediately
|
||||
// deleted too. In all other cases, the file will be moved to a trash
|
||||
// directory and scheduled for deletion by a background thread.
|
||||
// This API also supports assign a file to a specified bucket created by
|
||||
// `NewTrashBucket` when delete files in the background. So the caller can
|
||||
// wait for a specific bucket to be empty by checking the
|
||||
// `WaitForEmptyTrashBucket` API.
|
||||
Status DeleteUnaccountedFile(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg = false,
|
||||
std::optional<int32_t> bucket = std::nullopt);
|
||||
|
||||
// Wait for all files being deleted in the background to finish or for
|
||||
// destructor to be called.
|
||||
void WaitForEmptyTrash();
|
||||
|
||||
// Creates a new trash bucket. A bucket is only created and returned when slow
|
||||
// deletion is enabled.
|
||||
// For each bucket that is created, the user should also call
|
||||
// `WaitForEmptyTrashBucket` after scheduling file deletions to make sure the
|
||||
// trash files are all cleared.
|
||||
std::optional<int32_t> NewTrashBucket();
|
||||
|
||||
// Wait for all the files in the specified bucket to be deleted in the
|
||||
// background or for the destructor to be called.
|
||||
void WaitForEmptyTrashBucket(int32_t bucket);
|
||||
|
||||
// Return a map containing errors that happened in BackgroundEmptyTrash
|
||||
// file_path => error status
|
||||
std::map<std::string, Status> GetBackgroundErrors();
|
||||
@@ -87,12 +117,21 @@ class DeleteScheduler {
|
||||
}
|
||||
|
||||
private:
|
||||
Status MarkAsTrash(const std::string& file_path, std::string* path_in_trash);
|
||||
Status DeleteFileImmediately(const std::string& file_path, bool accounted);
|
||||
|
||||
Status AddFileToDeletionQueue(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
std::optional<int32_t> bucket, bool accounted);
|
||||
|
||||
Status MarkAsTrash(const std::string& file_path, bool accounted,
|
||||
std::string* path_in_trash);
|
||||
|
||||
Status DeleteTrashFile(const std::string& path_in_trash,
|
||||
const std::string& dir_to_sync,
|
||||
const std::string& dir_to_sync, bool accounted,
|
||||
uint64_t* deleted_bytes, bool* is_complete);
|
||||
|
||||
Status OnDeleteFile(const std::string& file_path, bool accounted);
|
||||
|
||||
void BackgroundEmptyTrash();
|
||||
|
||||
void MaybeCreateBackgroundThread();
|
||||
@@ -104,19 +143,28 @@ class DeleteScheduler {
|
||||
std::atomic<uint64_t> total_trash_size_;
|
||||
// Maximum number of bytes that should be deleted per second
|
||||
std::atomic<int64_t> rate_bytes_per_sec_;
|
||||
// Mutex to protect queue_, pending_files_, bg_errors_, closing_, stats_
|
||||
// Mutex to protect queue_, pending_files_, next_trash_bucket_,
|
||||
// pending_files_in_buckets_, bg_errors_, closing_, stats_
|
||||
InstrumentedMutex mu_;
|
||||
|
||||
struct FileAndDir {
|
||||
FileAndDir(const std::string& f, const std::string& d) : fname(f), dir(d) {}
|
||||
FileAndDir(const std::string& _fname, const std::string& _dir,
|
||||
bool _accounted, std::optional<int32_t> _bucket)
|
||||
: fname(_fname), dir(_dir), accounted(_accounted), bucket(_bucket) {}
|
||||
std::string fname;
|
||||
std::string dir; // empty will be skipped.
|
||||
bool accounted;
|
||||
std::optional<int32_t> bucket;
|
||||
};
|
||||
|
||||
// Queue of trash files that need to be deleted
|
||||
std::queue<FileAndDir> queue_;
|
||||
// Number of trash files that are waiting to be deleted
|
||||
int32_t pending_files_;
|
||||
// Next trash bucket that can be created
|
||||
int32_t next_trash_bucket_;
|
||||
// A mapping from trash bucket to number of pending files in the bucket
|
||||
std::map<int32_t, int32_t> pending_files_in_buckets_;
|
||||
uint64_t bytes_max_delete_chunk_;
|
||||
// Errors that happened in BackgroundEmptyTrash (file_path => error)
|
||||
std::map<std::string, Status> bg_errors_;
|
||||
@@ -127,6 +175,7 @@ class DeleteScheduler {
|
||||
// Condition variable signaled in these conditions
|
||||
// - pending_files_ value change from 0 => 1
|
||||
// - pending_files_ value change from 1 => 0
|
||||
// - a value in pending_files_in_buckets change from 1 => 0
|
||||
// - closing_ value is set to true
|
||||
InstrumentedCondVar cv_;
|
||||
// Background thread running BackgroundEmptyTrash
|
||||
@@ -138,6 +187,10 @@ class DeleteScheduler {
|
||||
// If the trash size constitutes for more than this fraction of the total DB
|
||||
// size we will start deleting new files passed to DeleteScheduler
|
||||
// immediately
|
||||
// Unaccounted files passed for deletion will not cause change in
|
||||
// total_trash_size_ or affect the DeleteScheduler::total_trash_size_ over
|
||||
// SstFileManager::total_size_ ratio. Their slow deletion is not subject to
|
||||
// this configured threshold either.
|
||||
std::atomic<double> max_trash_db_ratio_;
|
||||
static const uint64_t kMicrosInSecond = 1000 * 1000LL;
|
||||
std::shared_ptr<Statistics> stats_;
|
||||
|
||||
@@ -78,7 +78,7 @@ class DeleteSchedulerTest : public testing::Test {
|
||||
}
|
||||
|
||||
std::string NewDummyFile(const std::string& file_name, uint64_t size = 1024,
|
||||
size_t dummy_files_dirs_idx = 0) {
|
||||
size_t dummy_files_dirs_idx = 0, bool track = true) {
|
||||
std::string file_path =
|
||||
dummy_files_dirs_[dummy_files_dirs_idx] + "/" + file_name;
|
||||
std::unique_ptr<WritableFile> f;
|
||||
@@ -86,7 +86,9 @@ class DeleteSchedulerTest : public testing::Test {
|
||||
std::string data(size, 'A');
|
||||
EXPECT_OK(f->Append(data));
|
||||
EXPECT_OK(f->Close());
|
||||
EXPECT_OK(sst_file_mgr_->OnAddFile(file_path));
|
||||
if (track) {
|
||||
EXPECT_OK(sst_file_mgr_->OnAddFile(file_path));
|
||||
}
|
||||
return file_path;
|
||||
}
|
||||
|
||||
@@ -353,6 +355,8 @@ TEST_F(DeleteSchedulerTest, DisableRateLimiting) {
|
||||
ASSERT_EQ(num_files,
|
||||
stats_->getAndResetTickerCount(FILES_DELETED_IMMEDIATELY));
|
||||
|
||||
ASSERT_FALSE(delete_scheduler_->NewTrashBucket().has_value());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
@@ -718,6 +722,141 @@ TEST_F(DeleteSchedulerTest, IsTrashCheck) {
|
||||
ASSERT_FALSE(DeleteScheduler::IsTrashFile("abc.trashx"));
|
||||
}
|
||||
|
||||
TEST_F(DeleteSchedulerTest, DeleteAccountedAndUnaccountedFiles) {
|
||||
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / s
|
||||
NewDeleteScheduler();
|
||||
|
||||
// Create 100 files, every file is 1 KB
|
||||
int num_files = 100; // 100 files
|
||||
uint64_t file_size = 1024; // 1 KB as a file size
|
||||
std::vector<std::string> generated_files;
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
std::string file_name = "file" + std::to_string(i) + ".data";
|
||||
generated_files.push_back(NewDummyFile(file_name, file_size,
|
||||
/*dummy_files_dirs_idx*/ 0,
|
||||
/*track=*/false));
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
if (i % 2) {
|
||||
ASSERT_OK(sst_file_mgr_->OnAddFile(generated_files[i], file_size));
|
||||
ASSERT_OK(delete_scheduler_->DeleteFile(generated_files[i], ""));
|
||||
} else {
|
||||
ASSERT_OK(
|
||||
delete_scheduler_->DeleteUnaccountedFile(generated_files[i], ""));
|
||||
}
|
||||
}
|
||||
|
||||
delete_scheduler_->WaitForEmptyTrash();
|
||||
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
|
||||
ASSERT_EQ(0, sst_file_mgr_->GetTotalSize());
|
||||
}
|
||||
|
||||
TEST_F(DeleteSchedulerTest, ConcurrentlyDeleteUnaccountedFilesInBuckets) {
|
||||
int bg_delete_file = 0;
|
||||
int fg_delete_file = 0;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile:DeleteFile",
|
||||
[&](void* /*arg*/) { bg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteFile", [&](void* /*arg*/) { fg_delete_file++; });
|
||||
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / s
|
||||
NewDeleteScheduler();
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
// Create 1000 files, every file is 1 KB
|
||||
int num_files = 1000;
|
||||
uint64_t file_size = 1024; // 1 KB as a file size
|
||||
std::vector<std::string> generated_files;
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
std::string file_name = "file" + std::to_string(i) + ".data";
|
||||
generated_files.push_back(NewDummyFile(file_name, file_size,
|
||||
/*dummy_files_dirs_idx*/ 0,
|
||||
/*track=*/false));
|
||||
}
|
||||
// Concurrently delete files in different buckets and check all the buckets
|
||||
// are empty.
|
||||
int thread_cnt = 10;
|
||||
int files_per_thread = 100;
|
||||
std::atomic<int> thread_num(0);
|
||||
std::vector<port::Thread> threads;
|
||||
std::function<void()> delete_thread = [&]() {
|
||||
std::optional<int32_t> bucket = delete_scheduler_->NewTrashBucket();
|
||||
ASSERT_TRUE(bucket.has_value());
|
||||
int idx = thread_num.fetch_add(1);
|
||||
int range_start = idx * files_per_thread;
|
||||
int range_end = range_start + files_per_thread;
|
||||
for (int j = range_start; j < range_end; j++) {
|
||||
ASSERT_OK(delete_scheduler_->DeleteUnaccountedFile(
|
||||
generated_files[j], "", /*false_bg=*/false, bucket));
|
||||
}
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(bucket.value());
|
||||
};
|
||||
|
||||
for (int i = 0; i < thread_cnt; i++) {
|
||||
threads.emplace_back(delete_thread);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < threads.size(); i++) {
|
||||
threads[i].join();
|
||||
}
|
||||
|
||||
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
|
||||
ASSERT_EQ(0, stats_->getAndResetTickerCount(FILES_DELETED_IMMEDIATELY));
|
||||
ASSERT_EQ(1000, stats_->getAndResetTickerCount(FILES_MARKED_TRASH));
|
||||
ASSERT_EQ(0, fg_delete_file);
|
||||
ASSERT_EQ(1000, bg_delete_file);
|
||||
|
||||
// OK to re check an already empty bucket
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(9);
|
||||
// Invalid bucket return too.
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(100);
|
||||
std::optional<int32_t> next_bucket = delete_scheduler_->NewTrashBucket();
|
||||
ASSERT_TRUE(next_bucket.has_value());
|
||||
ASSERT_EQ(10, next_bucket.value());
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(10);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DeleteSchedulerTest,
|
||||
ImmediatelyDeleteUnaccountedFilesWithRemainingLinks) {
|
||||
int bg_delete_file = 0;
|
||||
int fg_delete_file = 0;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile:DeleteFile",
|
||||
[&](void* /*arg*/) { bg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteFile", [&](void* /*arg*/) { fg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / sec
|
||||
NewDeleteScheduler();
|
||||
|
||||
std::string file1 = NewDummyFile("data_1", 500 * 1024,
|
||||
/*dummy_files_dirs_idx*/ 0, /*track=*/false);
|
||||
std::string file2 = NewDummyFile("data_2", 100 * 1024,
|
||||
/*dummy_files_dirs_idx*/ 0, /*track=*/false);
|
||||
|
||||
ASSERT_OK(env_->LinkFile(file1, dummy_files_dirs_[0] + "/data_1b"));
|
||||
ASSERT_OK(env_->LinkFile(file2, dummy_files_dirs_[0] + "/data_2b"));
|
||||
|
||||
// Should delete in 4 batch if there is no hardlink
|
||||
ASSERT_OK(
|
||||
delete_scheduler_->DeleteUnaccountedFile(file1, "", /*force_bg=*/false));
|
||||
ASSERT_OK(
|
||||
delete_scheduler_->DeleteUnaccountedFile(file2, "", /*force_bg=*/false));
|
||||
|
||||
delete_scheduler_->WaitForEmptyTrash();
|
||||
|
||||
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
|
||||
ASSERT_EQ(0, bg_delete_file);
|
||||
ASSERT_EQ(2, fg_delete_file);
|
||||
ASSERT_EQ(0, stats_->getAndResetTickerCount(FILES_MARKED_TRASH));
|
||||
ASSERT_EQ(2, stats_->getAndResetTickerCount(FILES_DELETED_IMMEDIATELY));
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+17
-2
@@ -122,8 +122,8 @@ IOStatus CreateFile(FileSystem* fs, const std::string& destination,
|
||||
Status DeleteDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname, const std::string& dir_to_sync,
|
||||
const bool force_bg, const bool force_fg) {
|
||||
SstFileManagerImpl* sfm =
|
||||
static_cast<SstFileManagerImpl*>(db_options->sst_file_manager.get());
|
||||
SstFileManagerImpl* sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
db_options->sst_file_manager.get());
|
||||
if (sfm && !force_fg) {
|
||||
return sfm->ScheduleFileDeletion(fname, dir_to_sync, force_bg);
|
||||
} else {
|
||||
@@ -131,6 +131,21 @@ Status DeleteDBFile(const ImmutableDBOptions* db_options,
|
||||
}
|
||||
}
|
||||
|
||||
Status DeleteUnaccountedDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg, const bool force_fg,
|
||||
std::optional<int32_t> bucket) {
|
||||
SstFileManagerImpl* sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
db_options->sst_file_manager.get());
|
||||
if (sfm && !force_fg) {
|
||||
return sfm->ScheduleUnaccountedFileDeletion(fname, dir_to_sync, force_bg,
|
||||
bucket);
|
||||
} else {
|
||||
return db_options->env->DeleteFile(fname);
|
||||
}
|
||||
}
|
||||
|
||||
// requested_checksum_func_name brings the function name of the checksum
|
||||
// generator in checksum_factory. Empty string is permitted, in which case the
|
||||
// name of the generator created by the factory is unchecked. When
|
||||
|
||||
@@ -55,6 +55,16 @@ Status DeleteDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname, const std::string& path_to_sync,
|
||||
const bool force_bg, const bool force_fg);
|
||||
|
||||
// Delete an unaccounted DB file that is not tracked by SstFileManager and will
|
||||
// not be tracked by its DeleteScheduler when getting deleted.
|
||||
// If a legitimate bucket is provided and this file is scheduled for slow
|
||||
// deletion, it will be assigned to the specified trash bucket.
|
||||
Status DeleteUnaccountedDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg, const bool force_fg,
|
||||
std::optional<int32_t> bucket);
|
||||
|
||||
// TODO(hx235): pass the whole DBOptions intead of its individual fields
|
||||
IOStatus GenerateOneFileChecksum(
|
||||
FileSystem* fs, const std::string& file_path,
|
||||
|
||||
@@ -421,10 +421,28 @@ Status SstFileManagerImpl::ScheduleFileDeletion(const std::string& file_path,
|
||||
return delete_scheduler_.DeleteFile(file_path, path_to_sync, force_bg);
|
||||
}
|
||||
|
||||
Status SstFileManagerImpl::ScheduleUnaccountedFileDeletion(
|
||||
const std::string& file_path, const std::string& dir_to_sync,
|
||||
const bool force_bg, std::optional<int32_t> bucket) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion",
|
||||
const_cast<std::string*>(&file_path));
|
||||
return delete_scheduler_.DeleteUnaccountedFile(file_path, dir_to_sync,
|
||||
force_bg, bucket);
|
||||
}
|
||||
|
||||
void SstFileManagerImpl::WaitForEmptyTrash() {
|
||||
delete_scheduler_.WaitForEmptyTrash();
|
||||
}
|
||||
|
||||
std::optional<int32_t> SstFileManagerImpl::NewTrashBucket() {
|
||||
return delete_scheduler_.NewTrashBucket();
|
||||
}
|
||||
|
||||
void SstFileManagerImpl::WaitForEmptyTrashBucket(int32_t bucket) {
|
||||
delete_scheduler_.WaitForEmptyTrashBucket(bucket);
|
||||
}
|
||||
|
||||
void SstFileManagerImpl::OnAddFileImpl(const std::string& file_path,
|
||||
uint64_t file_size) {
|
||||
auto tracked_file = tracked_files_.find(file_path);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "db/compaction/compaction.h"
|
||||
@@ -118,17 +118,40 @@ class SstFileManagerImpl : public SstFileManager {
|
||||
// not guaranteed
|
||||
bool CancelErrorRecovery(ErrorHandler* db);
|
||||
|
||||
// Mark file as trash and schedule it's deletion. If force_bg is set, it
|
||||
// Mark a file as trash and schedule its deletion. If force_bg is set, it
|
||||
// forces the file to be deleting in the background regardless of DB size,
|
||||
// except when rate limited delete is disabled
|
||||
// except when rate limited delete is disabled.
|
||||
virtual Status ScheduleFileDeletion(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg = false);
|
||||
|
||||
// Wait for all files being deleteing in the background to finish or for
|
||||
// Delete an unaccounted file. The file is deleted immediately if slow
|
||||
// deletion is disabled. A file with more than 1 hard links will be deleted
|
||||
// immediately unless force_bg is set. In other cases, files will be scheduled
|
||||
// for slow deletion, and assigned to the specified bucket if a legitimate one
|
||||
// is provided. A legitimate bucket is one that is created with the
|
||||
// `NewTrashBucket` API, and for which `WaitForEmptyTrashBucket` hasn't been
|
||||
// called yet.
|
||||
virtual Status ScheduleUnaccountedFileDeletion(
|
||||
const std::string& file_path, const std::string& dir_to_sync,
|
||||
const bool force_bg = false,
|
||||
std::optional<int32_t> bucket = std::nullopt);
|
||||
|
||||
// Wait for all files being deleted in the background to finish or for
|
||||
// destructor to be called.
|
||||
virtual void WaitForEmptyTrash();
|
||||
|
||||
// Creates a new trash bucket. A legitimate bucket is only created and
|
||||
// returned when slow deletion is enabled.
|
||||
// For each bucket that is created and used, the user should also call
|
||||
// `WaitForEmptyTrashBucket` after scheduling file deletions to make sure all
|
||||
// the trash files are cleared.
|
||||
std::optional<int32_t> NewTrashBucket();
|
||||
|
||||
// Wait for all the files in the specified bucket to be deleted in the
|
||||
// background or for destructor to be called.
|
||||
virtual void WaitForEmptyTrashBucket(int32_t bucket);
|
||||
|
||||
DeleteScheduler* delete_scheduler() { return &delete_scheduler_; }
|
||||
|
||||
// Stop the error recovery background thread. This should be called only
|
||||
|
||||
@@ -1064,6 +1064,21 @@ rocksdb_block_based_options_set_pin_top_level_index_and_filter(
|
||||
rocksdb_block_based_table_options_t*, unsigned char);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_block_based_table_factory(
|
||||
rocksdb_options_t* opt, rocksdb_block_based_table_options_t* table_options);
|
||||
enum {
|
||||
rocksdb_block_based_k_fallback_pinning_tier = 0,
|
||||
rocksdb_block_based_k_none_pinning_tier = 1,
|
||||
rocksdb_block_based_k_flush_and_similar_pinning_tier = 2,
|
||||
rocksdb_block_based_k_all_pinning_tier = 3,
|
||||
};
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_top_level_index_pinning_tier(
|
||||
rocksdb_block_based_table_options_t*, int);
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_partition_pinning_tier(
|
||||
rocksdb_block_based_table_options_t*, int);
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_unpartitioned_pinning_tier(
|
||||
rocksdb_block_based_table_options_t*, int);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_write_buffer_manager(
|
||||
rocksdb_options_t* opt, rocksdb_write_buffer_manager_t* wbm);
|
||||
|
||||
@@ -1682,6 +1697,18 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_wal_compression(
|
||||
extern ROCKSDB_LIBRARY_API int rocksdb_options_get_wal_compression(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
enum {
|
||||
rocksdb_k_by_compensated_size_compaction_pri = 0,
|
||||
rocksdb_k_oldest_largest_seq_first_compaction_pri = 1,
|
||||
rocksdb_k_oldest_smallest_seq_first_compaction_pri = 2,
|
||||
rocksdb_k_min_overlapping_ratio_compaction_pri = 3,
|
||||
rocksdb_k_round_robin_compaction_pri = 4
|
||||
};
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_compaction_pri(
|
||||
rocksdb_options_t*, int);
|
||||
extern ROCKSDB_LIBRARY_API int rocksdb_options_get_compaction_pri(
|
||||
rocksdb_options_t*);
|
||||
|
||||
/* RateLimiter */
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_ratelimiter_t* rocksdb_ratelimiter_create(
|
||||
int64_t rate_bytes_per_sec, int64_t refill_period_us, int32_t fairness);
|
||||
|
||||
@@ -523,6 +523,11 @@ enum TieredAdmissionPolicy {
|
||||
// compressed secondary, and a compressed local flash (non-volatile) cache.
|
||||
// Each tier is managed as an independent queue.
|
||||
kAdmPolicyThreeQueue,
|
||||
// Allow all blocks evicted from the primary block cache into the secondary
|
||||
// cache. This may increase CPU overhead due to more blocks being admitted
|
||||
// and compressed, but may increase the compressed secondary cache hit rate
|
||||
// for some workloads
|
||||
kAdmPolicyAllowAll,
|
||||
kAdmPolicyMax,
|
||||
};
|
||||
|
||||
|
||||
@@ -142,6 +142,11 @@ class Comparator : public Customizable, public CompareInterface {
|
||||
return Slice();
|
||||
}
|
||||
|
||||
// Return a human readable user-defined timestamp for debugging.
|
||||
virtual std::string TimestampToString(const Slice& /*timestamp*/) const {
|
||||
return "";
|
||||
}
|
||||
|
||||
int CompareWithoutTimestamp(const Slice& a, const Slice& b) const {
|
||||
return CompareWithoutTimestamp(a, /*a_has_ts=*/true, b, /*b_has_ts=*/true);
|
||||
}
|
||||
|
||||
@@ -1475,6 +1475,9 @@ class DB {
|
||||
// move the files back to the minimum level capable of holding the data set
|
||||
// or a given level (specified by non-negative options.target_level).
|
||||
//
|
||||
// For FIFO compaction, this will trigger a compaction (if available)
|
||||
// based on CompactionOptionsFIFO.
|
||||
//
|
||||
// In case of user-defined timestamp, if enabled, `begin` and `end` should
|
||||
// not contain timestamp.
|
||||
virtual Status CompactRange(const CompactRangeOptions& options,
|
||||
@@ -1676,8 +1679,8 @@ class DB {
|
||||
// Freezes the logical state of the DB (by stopping writes), and if WAL is
|
||||
// enabled, ensures that state has been flushed to DB files (as in
|
||||
// FlushWAL()). This can be used for taking a Checkpoint at a known DB
|
||||
// state, though the user must use options to insure no DB flush is invoked
|
||||
// in this frozen state. Other operations allowed on a "read only" DB should
|
||||
// state, though while the WAL is locked, flushes as part of CreateCheckpoint
|
||||
// and simiar are skipped. Other operations allowed on a "read only" DB should
|
||||
// work while frozen. Each LockWAL() call that returns OK must eventually be
|
||||
// followed by a corresponding call to UnlockWAL(). Where supported, non-OK
|
||||
// status is generally only possible with some kind of corruption or I/O
|
||||
@@ -1858,6 +1861,7 @@ class DB {
|
||||
// supported. 4) When an ingested file contains point data and range deletion
|
||||
// for the same key, the point data currently overrides the range deletion
|
||||
// regardless which one has the higher user-defined timestamps.
|
||||
// For FIFO compaction, SST files will always be ingested into L0.
|
||||
//
|
||||
// (1) External SST files can be created using SstFileWriter
|
||||
// (2) We will try to ingest the files to the lowest possible level
|
||||
@@ -2005,6 +2009,8 @@ class DB {
|
||||
return Status::NotSupported("SuggestCompactRange() is not implemented.");
|
||||
}
|
||||
|
||||
// Trivially move L0 files to target level. Should not be called with another
|
||||
// PromoteL0() concurrently
|
||||
virtual Status PromoteL0(ColumnFamilyHandle* /*column_family*/,
|
||||
int /*target_level*/) {
|
||||
return Status::NotSupported("PromoteL0() is not implemented.");
|
||||
|
||||
@@ -32,7 +32,7 @@ class Iterator : public IteratorBase {
|
||||
Iterator(const Iterator&) = delete;
|
||||
void operator=(const Iterator&) = delete;
|
||||
|
||||
virtual ~Iterator() {}
|
||||
virtual ~Iterator() override {}
|
||||
|
||||
// Return the value for the current entry. If the entry is a plain key-value,
|
||||
// return the value as-is; if it is a wide-column entity, return the value of
|
||||
@@ -78,6 +78,12 @@ class Iterator : public IteratorBase {
|
||||
// - Iterator created with ReadOptions::pin_data = true
|
||||
// - DB tables were created with
|
||||
// BlockBasedTableOptions::use_delta_encoding = false.
|
||||
// Property "rocksdb.iterator.is-value-pinned":
|
||||
// If returning "1", this means that the Slice returned by value() is valid
|
||||
// as long as the iterator is not deleted.
|
||||
// It is guaranteed to always return "1" if
|
||||
// - Iterator created with ReadOptions::pin_data = true
|
||||
// - The value is found in a `kTypeValue` record
|
||||
// Property "rocksdb.iterator.super-version-number":
|
||||
// LSM version used by the iterator. The same format as DB Property
|
||||
// kCurrentSuperVersionNumber. See its comment for more information.
|
||||
|
||||
@@ -67,6 +67,7 @@ enum EntryType {
|
||||
kEntryBlobIndex,
|
||||
kEntryDeleteWithTimestamp,
|
||||
kEntryWideColumnEntity,
|
||||
kEntryTimedPut, // That hasn't yet converted to a standard Put entry
|
||||
kEntryOther,
|
||||
};
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ class LDBCommand {
|
||||
static const std::string ARG_PREPOPULATE_BLOB_CACHE;
|
||||
static const std::string ARG_DECODE_BLOB_INDEX;
|
||||
static const std::string ARG_DUMP_UNCOMPRESSED_BLOBS;
|
||||
static const std::string ARG_READ_TIMESTAMP;
|
||||
|
||||
struct ParsedParams {
|
||||
std::string cmd;
|
||||
@@ -190,6 +191,9 @@ class LDBCommand {
|
||||
|
||||
bool create_if_missing_;
|
||||
|
||||
/** Encoded user provided uint64_t read timestamp. */
|
||||
std::string read_timestamp_;
|
||||
|
||||
/**
|
||||
* Map of options passed on the command-line.
|
||||
*/
|
||||
@@ -275,6 +279,10 @@ class LDBCommand {
|
||||
bool ParseBooleanOption(const std::map<std::string, std::string>& options,
|
||||
const std::string& option, bool default_val);
|
||||
|
||||
/* Populate `ropts.timestamp` from command line flag --read_timestamp */
|
||||
Status MaybePopulateReadTimestamp(ColumnFamilyHandle* cfh, ReadOptions& ropts,
|
||||
Slice* read_timestamp);
|
||||
|
||||
Options options_;
|
||||
std::vector<ColumnFamilyDescriptor> column_families_;
|
||||
ConfigOptions config_options_;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -165,7 +164,7 @@ class Transaction {
|
||||
virtual void SetSnapshot() = 0;
|
||||
|
||||
// Similar to SetSnapshot(), but will not change the current snapshot
|
||||
// until Put/Merge/Delete/GetForUpdate/MultigetForUpdate is called.
|
||||
// until Put/PutEntity/Merge/Delete/GetForUpdate/MultigetForUpdate is called.
|
||||
// By calling this function, the transaction will essentially call
|
||||
// SetSnapshot() for you right before performing the next write/GetForUpdate.
|
||||
//
|
||||
@@ -268,10 +267,10 @@ class Transaction {
|
||||
// points.
|
||||
virtual void SetSavePoint() = 0;
|
||||
|
||||
// Undo all operations in this transaction (Put, Merge, Delete, PutLogData)
|
||||
// since the most recent call to SetSavePoint() and removes the most recent
|
||||
// SetSavePoint().
|
||||
// If there is no previous call to SetSavePoint(), returns Status::NotFound()
|
||||
// Undo all operations in this transaction (Put, PutEntity, Merge, Delete,
|
||||
// PutLogData) since the most recent call to SetSavePoint() and removes the
|
||||
// most recent SetSavePoint(). If there is no previous call to SetSavePoint(),
|
||||
// returns Status::NotFound()
|
||||
virtual Status RollbackToSavePoint() = 0;
|
||||
|
||||
// Pop the most recent save point.
|
||||
@@ -318,6 +317,10 @@ class Transaction {
|
||||
return s;
|
||||
}
|
||||
|
||||
virtual Status GetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableWideColumns* columns) = 0;
|
||||
|
||||
virtual std::vector<Status> MultiGet(
|
||||
const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_family,
|
||||
@@ -354,6 +357,12 @@ class Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
virtual void MultiGetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
size_t num_keys, const Slice* keys,
|
||||
PinnableWideColumns* results, Status* statuses,
|
||||
bool sorted_input = false) = 0;
|
||||
|
||||
// Read this key and ensure that this transaction will only
|
||||
// be able to be committed if this key is not written outside this
|
||||
// transaction after it has first been read (or after the snapshot if a
|
||||
@@ -461,9 +470,9 @@ class Transaction {
|
||||
virtual Iterator* GetIterator(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family) = 0;
|
||||
|
||||
// Put, Merge, Delete, and SingleDelete behave similarly to the corresponding
|
||||
// functions in WriteBatch, but will also do conflict checking on the
|
||||
// keys being written.
|
||||
// Put, PutEntity, Merge, Delete, and SingleDelete behave similarly to the
|
||||
// corresponding functions in WriteBatch, but will also do conflict checking
|
||||
// on the keys being written.
|
||||
//
|
||||
// assume_tracked=true expects the key be already tracked. More
|
||||
// specifically, it means the the key was previous tracked in the same
|
||||
@@ -489,6 +498,10 @@ class Transaction {
|
||||
const bool assume_tracked = false) = 0;
|
||||
virtual Status Put(const SliceParts& key, const SliceParts& value) = 0;
|
||||
|
||||
virtual Status PutEntity(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns,
|
||||
bool assume_tracked = false) = 0;
|
||||
|
||||
virtual Status Merge(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const Slice& value,
|
||||
const bool assume_tracked = false) = 0;
|
||||
@@ -528,6 +541,10 @@ class Transaction {
|
||||
virtual Status PutUntracked(const SliceParts& key,
|
||||
const SliceParts& value) = 0;
|
||||
|
||||
virtual Status PutEntityUntracked(ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
const WideColumns& columns) = 0;
|
||||
|
||||
virtual Status MergeUntracked(ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& value) = 0;
|
||||
virtual Status MergeUntracked(const Slice& key, const Slice& value) = 0;
|
||||
@@ -556,18 +573,18 @@ class Transaction {
|
||||
// Similar to WriteBatch::PutLogData
|
||||
virtual void PutLogData(const Slice& blob) = 0;
|
||||
|
||||
// By default, all Put/Merge/Delete operations will be indexed in the
|
||||
// transaction so that Get/GetForUpdate/GetIterator can search for these
|
||||
// By default, all Put/PutEntity/Merge/Delete operations will be indexed in
|
||||
// the transaction so that Get/GetForUpdate/GetIterator can search for these
|
||||
// keys.
|
||||
//
|
||||
// If the caller does not want to fetch the keys about to be written,
|
||||
// they may want to avoid indexing as a performance optimization.
|
||||
// Calling DisableIndexing() will turn off indexing for all future
|
||||
// Put/Merge/Delete operations until EnableIndexing() is called.
|
||||
// Put/PutEntity/Merge/Delete operations until EnableIndexing() is called.
|
||||
//
|
||||
// If a key is Put/Merge/Deleted after DisableIndexing is called and then
|
||||
// is fetched via Get/GetForUpdate/GetIterator, the result of the fetch is
|
||||
// undefined.
|
||||
// If a key is written (using Put/PutEntity/Merge/Delete) after
|
||||
// DisableIndexing is called and then is fetched via
|
||||
// Get/GetForUpdate/GetIterator, the result of the fetch is undefined.
|
||||
virtual void DisableIndexing() = 0;
|
||||
virtual void EnableIndexing() = 0;
|
||||
|
||||
@@ -578,9 +595,10 @@ class Transaction {
|
||||
// number of keys that need to be checked for conflicts at commit time.
|
||||
virtual uint64_t GetNumKeys() const = 0;
|
||||
|
||||
// Returns the number of Puts/Deletes/Merges that have been applied to this
|
||||
// transaction so far.
|
||||
// Returns the number of Put/PutEntity/Delete/Merge operations that have been
|
||||
// applied to this transaction so far.
|
||||
virtual uint64_t GetNumPuts() const = 0;
|
||||
virtual uint64_t GetNumPutEntities() const = 0;
|
||||
virtual uint64_t GetNumDeletes() const = 0;
|
||||
virtual uint64_t GetNumMerges() const = 0;
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class WriteBatchWithIndex : public WriteBatchBase {
|
||||
"Cannot call this method without attribute groups");
|
||||
}
|
||||
return Status::NotSupported(
|
||||
"PutEntity not supported by WriteBatchWithIndex");
|
||||
"PutEntity with AttributeGroups not supported by WriteBatchWithIndex");
|
||||
}
|
||||
|
||||
using WriteBatchBase::Merge;
|
||||
@@ -284,7 +284,12 @@ class WriteBatchWithIndex : public WriteBatchBase {
|
||||
Status GetEntityFromBatchAndDB(DB* db, const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns);
|
||||
PinnableWideColumns* columns) {
|
||||
constexpr ReadCallback* callback = nullptr;
|
||||
|
||||
return GetEntityFromBatchAndDB(db, read_options, column_family, key,
|
||||
columns, callback);
|
||||
}
|
||||
|
||||
void MultiGetFromBatchAndDB(DB* db, const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
@@ -310,7 +315,13 @@ class WriteBatchWithIndex : public WriteBatchBase {
|
||||
ColumnFamilyHandle* column_family,
|
||||
size_t num_keys, const Slice* keys,
|
||||
PinnableWideColumns* results,
|
||||
Status* statuses, bool sorted_input);
|
||||
Status* statuses, bool sorted_input) {
|
||||
constexpr ReadCallback* callback = nullptr;
|
||||
|
||||
MultiGetEntityFromBatchAndDB(db, read_options, column_family, num_keys,
|
||||
keys, results, statuses, sorted_input,
|
||||
callback);
|
||||
}
|
||||
|
||||
// Records the state of the batch for future calls to RollbackToSavePoint().
|
||||
// May be called multiple times to set multiple save points.
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// NOTE: in 'main' development branch, this should be the *next*
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 9
|
||||
#define ROCKSDB_MINOR 2
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_MINOR 3
|
||||
#define ROCKSDB_PATCH 2
|
||||
|
||||
// Do not use these. We made the mistake of declaring macros starting with
|
||||
// double underscore. Now we have to live with our choice. We'll deprecate these
|
||||
|
||||
@@ -338,8 +338,8 @@ void BlockBasedTableIterator::InitDataBlock() {
|
||||
bool use_block_cache_for_lookup = true;
|
||||
|
||||
if (DoesContainBlockHandles()) {
|
||||
data_block_handle = block_handles_.front().handle_;
|
||||
is_in_cache = block_handles_.front().is_cache_hit_;
|
||||
data_block_handle = block_handles_->front().handle_;
|
||||
is_in_cache = block_handles_->front().is_cache_hit_;
|
||||
use_block_cache_for_lookup = false;
|
||||
} else {
|
||||
data_block_handle = index_iter_->value().handle;
|
||||
@@ -361,7 +361,7 @@ void BlockBasedTableIterator::InitDataBlock() {
|
||||
Status s;
|
||||
block_iter_.Invalidate(Status::OK());
|
||||
table_->NewDataBlockIterator<DataBlockIter>(
|
||||
read_options_, (block_handles_.front().cachable_entry_).As<Block>(),
|
||||
read_options_, (block_handles_->front().cachable_entry_).As<Block>(),
|
||||
&block_iter_, s);
|
||||
} else {
|
||||
auto* rep = table_->get_rep();
|
||||
@@ -466,8 +466,8 @@ void BlockBasedTableIterator::AsyncInitDataBlock(bool is_first_pass) {
|
||||
bool is_in_cache = false;
|
||||
|
||||
if (DoesContainBlockHandles()) {
|
||||
data_block_handle = block_handles_.front().handle_;
|
||||
is_in_cache = block_handles_.front().is_cache_hit_;
|
||||
data_block_handle = block_handles_->front().handle_;
|
||||
is_in_cache = block_handles_->front().is_cache_hit_;
|
||||
} else {
|
||||
data_block_handle = index_iter_->value().handle;
|
||||
}
|
||||
@@ -477,7 +477,7 @@ void BlockBasedTableIterator::AsyncInitDataBlock(bool is_first_pass) {
|
||||
if (is_in_cache) {
|
||||
block_iter_.Invalidate(Status::OK());
|
||||
table_->NewDataBlockIterator<DataBlockIter>(
|
||||
read_options_, (block_handles_.front().cachable_entry_).As<Block>(),
|
||||
read_options_, (block_handles_->front().cachable_entry_).As<Block>(),
|
||||
&block_iter_, s);
|
||||
} else {
|
||||
table_->NewDataBlockIterator<DataBlockIter>(
|
||||
@@ -524,7 +524,7 @@ bool BlockBasedTableIterator::MaterializeCurrentBlock() {
|
||||
// BlockCacheLookupForReadAheadSize is called.
|
||||
Slice first_internal_key;
|
||||
if (DoesContainBlockHandles()) {
|
||||
first_internal_key = block_handles_.front().first_internal_key_;
|
||||
first_internal_key = block_handles_->front().first_internal_key_;
|
||||
} else {
|
||||
first_internal_key = index_iter_->value().first_internal_key;
|
||||
}
|
||||
@@ -580,7 +580,7 @@ void BlockBasedTableIterator::FindBlockForward() {
|
||||
if (DoesContainBlockHandles()) {
|
||||
// Advance and point to that next Block handle to make that block handle
|
||||
// current.
|
||||
block_handles_.pop_front();
|
||||
block_handles_->pop_front();
|
||||
}
|
||||
|
||||
if (!DoesContainBlockHandles()) {
|
||||
@@ -681,7 +681,8 @@ void BlockBasedTableIterator::InitializeStartAndEndOffsets(
|
||||
bool read_curr_block, bool& found_first_miss_block,
|
||||
uint64_t& start_updated_offset, uint64_t& end_updated_offset,
|
||||
size_t& prev_handles_size) {
|
||||
prev_handles_size = block_handles_.size();
|
||||
assert(block_handles_ != nullptr);
|
||||
prev_handles_size = block_handles_->size();
|
||||
size_t footer = table_->get_rep()->footer.GetBlockTrailerSize();
|
||||
|
||||
// It initialize start and end offset to begin which is covered by following
|
||||
@@ -701,7 +702,7 @@ void BlockBasedTableIterator::InitializeStartAndEndOffsets(
|
||||
|
||||
end_updated_offset = block_handle_info.handle_.offset() + footer +
|
||||
block_handle_info.handle_.size();
|
||||
block_handles_.emplace_back(std::move(block_handle_info));
|
||||
block_handles_->emplace_back(std::move(block_handle_info));
|
||||
|
||||
index_iter_->Next();
|
||||
is_index_at_curr_block_ = false;
|
||||
@@ -717,17 +718,17 @@ void BlockBasedTableIterator::InitializeStartAndEndOffsets(
|
||||
// Initialize prev_handles_size to 0 as all those handles need to be read
|
||||
// again.
|
||||
prev_handles_size = 0;
|
||||
start_updated_offset = block_handles_.front().handle_.offset();
|
||||
end_updated_offset = block_handles_.back().handle_.offset() + footer +
|
||||
block_handles_.back().handle_.size();
|
||||
start_updated_offset = block_handles_->front().handle_.offset();
|
||||
end_updated_offset = block_handles_->back().handle_.offset() + footer +
|
||||
block_handles_->back().handle_.size();
|
||||
}
|
||||
} else {
|
||||
// Scenario 3 : read_curr_block is false (callback made to do additional
|
||||
// prefetching in buffers) and the queue already has some
|
||||
// handles from first buffer.
|
||||
if (DoesContainBlockHandles()) {
|
||||
start_updated_offset = block_handles_.back().handle_.offset() + footer +
|
||||
block_handles_.back().handle_.size();
|
||||
start_updated_offset = block_handles_->back().handle_.offset() + footer +
|
||||
block_handles_->back().handle_.size();
|
||||
end_updated_offset = start_updated_offset;
|
||||
} else {
|
||||
// Scenario 4 : read_curr_block is false (callback made to do additional
|
||||
@@ -789,6 +790,9 @@ void BlockBasedTableIterator::BlockCacheLookupForReadAheadSize(
|
||||
|
||||
// Initialize start and end offsets based on exisiting handles in the queue
|
||||
// and read_curr_block argument passed.
|
||||
if (block_handles_ == nullptr) {
|
||||
block_handles_.reset(new std::deque<BlockHandleInfo>());
|
||||
}
|
||||
InitializeStartAndEndOffsets(read_curr_block, found_first_miss_block,
|
||||
start_updated_offset, end_updated_offset,
|
||||
prev_handles_size);
|
||||
@@ -833,7 +837,7 @@ void BlockBasedTableIterator::BlockCacheLookupForReadAheadSize(
|
||||
}
|
||||
|
||||
// Add the handle to the queue.
|
||||
block_handles_.emplace_back(std::move(block_handle_info));
|
||||
block_handles_->emplace_back(std::move(block_handle_info));
|
||||
|
||||
// Can't figure out for current block if current block
|
||||
// is out of bound. But for next block we can find that.
|
||||
@@ -851,9 +855,9 @@ void BlockBasedTableIterator::BlockCacheLookupForReadAheadSize(
|
||||
if (found_first_miss_block) {
|
||||
// Iterate cache hit block handles from the end till a Miss is there, to
|
||||
// truncate and update the end offset till that Miss.
|
||||
auto it = block_handles_.rbegin();
|
||||
auto it = block_handles_->rbegin();
|
||||
auto it_end =
|
||||
block_handles_.rbegin() + (block_handles_.size() - prev_handles_size);
|
||||
block_handles_->rbegin() + (block_handles_->size() - prev_handles_size);
|
||||
|
||||
while (it != it_end && (*it).is_cache_hit_ &&
|
||||
start_updated_offset != (*it).handle_.offset()) {
|
||||
|
||||
@@ -338,7 +338,8 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
// different blocks when readahead_size is calculated in
|
||||
// BlockCacheLookupForReadAheadSize, to avoid index_iter_ reseek,
|
||||
// block_handles_ is used.
|
||||
std::deque<BlockHandleInfo> block_handles_;
|
||||
// `block_handles_` is lazily constructed to save CPU when it is unused
|
||||
std::unique_ptr<std::deque<BlockHandleInfo>> block_handles_;
|
||||
|
||||
// During cache lookup to find readahead size, index_iter_ is iterated and it
|
||||
// can point to a different block. is_index_at_curr_block_ keeps track of
|
||||
@@ -418,7 +419,11 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
: false);
|
||||
}
|
||||
|
||||
void ClearBlockHandles() { block_handles_.clear(); }
|
||||
void ClearBlockHandles() {
|
||||
if (block_handles_ != nullptr) {
|
||||
block_handles_->clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset prev_block_offset_. If index_iter_ has moved ahead, it won't get
|
||||
// accurate prev_block_offset_.
|
||||
@@ -426,7 +431,9 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
prev_block_offset_ = std::numeric_limits<uint64_t>::max();
|
||||
}
|
||||
|
||||
bool DoesContainBlockHandles() { return !block_handles_.empty(); }
|
||||
bool DoesContainBlockHandles() {
|
||||
return block_handles_ != nullptr && !block_handles_->empty();
|
||||
}
|
||||
|
||||
void InitializeStartAndEndOffsets(bool read_curr_block,
|
||||
bool& found_first_miss_block,
|
||||
|
||||
+8
-10
@@ -241,7 +241,7 @@ inline void BlockFetcher::GetBlockContents() {
|
||||
// Read a block from the file and verify its checksum. Upon return, io_status_
|
||||
// will be updated with the status of the read, and slice_ will be updated
|
||||
// with a pointer to the data.
|
||||
void BlockFetcher::ReadBlock(bool retry, FSAllocationPtr& fs_buf) {
|
||||
void BlockFetcher::ReadBlock(bool retry) {
|
||||
FSReadRequest read_req;
|
||||
IOOptions opts;
|
||||
io_status_ = file_->PrepareIOOptions(read_options_, opts);
|
||||
@@ -336,7 +336,7 @@ void BlockFetcher::ReadBlock(bool retry, FSAllocationPtr& fs_buf) {
|
||||
|
||||
if (io_status_.ok()) {
|
||||
InsertCompressedBlockToPersistentCacheIfNeeded();
|
||||
fs_buf = std::move(read_req.fs_scratch);
|
||||
fs_buf_ = std::move(read_req.fs_scratch);
|
||||
} else {
|
||||
ReleaseFileSystemProvidedBuffer(&read_req);
|
||||
direct_io_buf_.reset();
|
||||
@@ -347,7 +347,6 @@ void BlockFetcher::ReadBlock(bool retry, FSAllocationPtr& fs_buf) {
|
||||
}
|
||||
|
||||
IOStatus BlockFetcher::ReadBlockContents() {
|
||||
FSAllocationPtr fs_buf;
|
||||
if (TryGetUncompressBlockFromPersistentCache()) {
|
||||
compression_type_ = kNoCompression;
|
||||
#ifndef NDEBUG
|
||||
@@ -360,15 +359,15 @@ IOStatus BlockFetcher::ReadBlockContents() {
|
||||
return io_status_;
|
||||
}
|
||||
} else if (!TryGetSerializedBlockFromPersistentCache()) {
|
||||
ReadBlock(/*retry =*/false, fs_buf);
|
||||
ReadBlock(/*retry =*/false);
|
||||
// If the file system supports retry after corruption, then try to
|
||||
// re-read the block and see if it succeeds.
|
||||
if (io_status_.IsCorruption() && retry_corrupt_read_) {
|
||||
assert(!fs_buf);
|
||||
ReadBlock(/*retry=*/true, fs_buf);
|
||||
assert(!fs_buf_);
|
||||
ReadBlock(/*retry=*/true);
|
||||
}
|
||||
if (!io_status_.ok()) {
|
||||
assert(!fs_buf);
|
||||
assert(!fs_buf_);
|
||||
return io_status_;
|
||||
}
|
||||
}
|
||||
@@ -417,16 +416,15 @@ IOStatus BlockFetcher::ReadAsyncBlockContents() {
|
||||
return io_s;
|
||||
}
|
||||
if (io_s.ok()) {
|
||||
FSAllocationPtr fs_buf;
|
||||
// Data Block is already in prefetch.
|
||||
got_from_prefetch_buffer_ = true;
|
||||
ProcessTrailerIfPresent();
|
||||
if (io_status_.IsCorruption() && retry_corrupt_read_) {
|
||||
got_from_prefetch_buffer_ = false;
|
||||
ReadBlock(/*retry = */ true, fs_buf);
|
||||
ReadBlock(/*retry = */ true);
|
||||
}
|
||||
if (!io_status_.ok()) {
|
||||
assert(!fs_buf);
|
||||
assert(!fs_buf_);
|
||||
return io_status_;
|
||||
}
|
||||
used_buf_ = const_cast<char*>(slice_.data());
|
||||
|
||||
@@ -137,6 +137,7 @@ class BlockFetcher {
|
||||
bool for_compaction_ = false;
|
||||
bool use_fs_scratch_ = false;
|
||||
bool retry_corrupt_read_ = false;
|
||||
FSAllocationPtr fs_buf_;
|
||||
|
||||
// return true if found
|
||||
bool TryGetUncompressBlockFromPersistentCache();
|
||||
@@ -152,7 +153,7 @@ class BlockFetcher {
|
||||
void InsertCompressedBlockToPersistentCacheIfNeeded();
|
||||
void InsertUncompressedBlockToPersistentCacheIfNeeded();
|
||||
void ProcessTrailerIfPresent();
|
||||
void ReadBlock(bool retry, FSAllocationPtr& fs_buf);
|
||||
void ReadBlock(bool retry);
|
||||
|
||||
void ReleaseFileSystemProvidedBuffer(FSReadRequest* read_req) {
|
||||
if (use_fs_scratch_) {
|
||||
|
||||
@@ -23,6 +23,10 @@ Status Iterator::GetProperty(std::string prop_name, std::string* prop) {
|
||||
*prop = "0";
|
||||
return Status::OK();
|
||||
}
|
||||
if (prop_name == "rocksdb.iterator.is-value-pinned") {
|
||||
*prop = "0";
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::InvalidArgument("Unidentified property.");
|
||||
}
|
||||
|
||||
|
||||
@@ -521,22 +521,22 @@ Status SstFileDumper::ReadSequential(bool print_kv, uint64_t read_num_limit,
|
||||
iter->value(), oss, output_hex_);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "%s => error deserializing wide columns\n",
|
||||
ikey.DebugString(true, output_hex_).c_str());
|
||||
ikey.DebugString(true, output_hex_, ucmp).c_str());
|
||||
continue;
|
||||
}
|
||||
fprintf(stdout, "%s => %s\n",
|
||||
ikey.DebugString(true, output_hex_).c_str(),
|
||||
ikey.DebugString(true, output_hex_, ucmp).c_str(),
|
||||
oss.str().c_str());
|
||||
} else if (ikey.type == kTypeValuePreferredSeqno) {
|
||||
auto [unpacked_value, preferred_seqno] =
|
||||
ParsePackedValueWithSeqno(value);
|
||||
fprintf(stdout, "%s => %s, %llu\n",
|
||||
ikey.DebugString(true, output_hex_).c_str(),
|
||||
ikey.DebugString(true, output_hex_, ucmp).c_str(),
|
||||
unpacked_value.ToString(output_hex_).c_str(),
|
||||
static_cast<unsigned long long>(preferred_seqno));
|
||||
} else {
|
||||
fprintf(stdout, "%s => %s\n",
|
||||
ikey.DebugString(true, output_hex_).c_str(),
|
||||
ikey.DebugString(true, output_hex_, ucmp).c_str(),
|
||||
value.ToString(output_hex_).c_str());
|
||||
}
|
||||
} else {
|
||||
@@ -545,12 +545,12 @@ Status SstFileDumper::ReadSequential(bool print_kv, uint64_t read_num_limit,
|
||||
const Status s = blob_index.DecodeFrom(value);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "%s => error decoding blob index\n",
|
||||
ikey.DebugString(true, output_hex_).c_str());
|
||||
ikey.DebugString(true, output_hex_, ucmp).c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(stdout, "%s => %s\n",
|
||||
ikey.DebugString(true, output_hex_).c_str(),
|
||||
ikey.DebugString(true, output_hex_, ucmp).c_str(),
|
||||
blob_index.DebugString(output_hex_).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +565,30 @@ void DeleteDir(Env* env, const std::string& dirname) {
|
||||
TryDeleteDir(env, dirname).PermitUncheckedError();
|
||||
}
|
||||
|
||||
FileType GetFileType(const std::string& path) {
|
||||
FileType type = kTempFile;
|
||||
std::size_t found = path.find_last_of('/');
|
||||
if (found == std::string::npos) {
|
||||
found = 0;
|
||||
}
|
||||
std::string file_name = path.substr(found);
|
||||
uint64_t number = 0;
|
||||
ParseFileName(file_name, &number, &type);
|
||||
return type;
|
||||
}
|
||||
|
||||
uint64_t GetFileNumber(const std::string& path) {
|
||||
FileType type = kTempFile;
|
||||
std::size_t found = path.find_last_of('/');
|
||||
if (found == std::string::npos) {
|
||||
found = 0;
|
||||
}
|
||||
std::string file_name = path.substr(found);
|
||||
uint64_t number = 0;
|
||||
ParseFileName(file_name, &number, &type);
|
||||
return number;
|
||||
}
|
||||
|
||||
Status CreateEnvFromSystem(const ConfigOptions& config_options, Env** result,
|
||||
std::shared_ptr<Env>* guard) {
|
||||
const char* env_uri = getenv("TEST_ENV_URI");
|
||||
|
||||
@@ -882,6 +882,12 @@ Status TryDeleteDir(Env* env, const std::string& dirname);
|
||||
// Delete a directory if it exists
|
||||
void DeleteDir(Env* env, const std::string& dirname);
|
||||
|
||||
// Find the FileType from the file path
|
||||
FileType GetFileType(const std::string& path);
|
||||
|
||||
// Get the file number given the file path
|
||||
uint64_t GetFileNumber(const std::string& path);
|
||||
|
||||
// Creates an Env from the system environment by looking at the system
|
||||
// environment variables.
|
||||
Status CreateEnvFromSystem(const ConfigOptions& options, Env** result,
|
||||
|
||||
@@ -125,7 +125,7 @@ EOF
|
||||
|
||||
# To check for DB forward compatibility with loading options (old version
|
||||
# reading data from new), as well as backward compatibility
|
||||
declare -a db_forward_with_options_refs=("8.6.fb" "8.7.fb" "8.8.fb" "8.9.fb" "8.10.fb" "8.11.fb" "9.0.fb" "9.1.fb")
|
||||
declare -a db_forward_with_options_refs=("8.6.fb" "8.7.fb" "8.8.fb" "8.9.fb" "8.10.fb" "8.11.fb" "9.0.fb" "9.1.fb" "9.2.fb")
|
||||
# To check for DB forward compatibility without loading options (in addition
|
||||
# to the "with loading options" set), as well as backward compatibility
|
||||
declare -a db_forward_no_options_refs=() # N/A at the moment
|
||||
|
||||
@@ -1299,6 +1299,8 @@ static enum ROCKSDB_NAMESPACE::TieredAdmissionPolicy StringToAdmissionPolicy(
|
||||
return ROCKSDB_NAMESPACE::kAdmPolicyAllowCacheHits;
|
||||
} else if (!strcasecmp(policy, "three_queue")) {
|
||||
return ROCKSDB_NAMESPACE::kAdmPolicyThreeQueue;
|
||||
} else if (!strcasecmp(policy, "allow_all")) {
|
||||
return ROCKSDB_NAMESPACE::kAdmPolicyAllowAll;
|
||||
} else {
|
||||
fprintf(stderr, "Cannot parse admission policy %s\n", policy);
|
||||
exit(1);
|
||||
|
||||
+11
-5
@@ -67,7 +67,11 @@ default_params = {
|
||||
"clear_column_family_one_in": 0,
|
||||
"compact_files_one_in": lambda: random.choice([1000, 1000000]),
|
||||
"compact_range_one_in": lambda: random.choice([1000, 1000000]),
|
||||
# Disabled because of various likely related failures with
|
||||
# "Cannot delete table file #N from level 0 since it is on level X"
|
||||
"promote_l0_one_in": 0,
|
||||
"compaction_pri": random.randint(0, 4),
|
||||
"key_may_exist_one_in": lambda: random.choice([100, 100000]),
|
||||
"data_block_index_type": lambda: random.choice([0, 1]),
|
||||
"delpercent": 4,
|
||||
"delrangepercent": 1,
|
||||
@@ -116,6 +120,7 @@ default_params = {
|
||||
"optimize_filters_for_memory": lambda: random.randint(0, 1),
|
||||
"partition_filters": lambda: random.randint(0, 1),
|
||||
"partition_pinning": lambda: random.randint(0, 3),
|
||||
"reset_stats_one_in": lambda: random.choice([10000, 1000000]),
|
||||
"pause_background_one_in": lambda: random.choice([10000, 1000000]),
|
||||
"disable_file_deletions_one_in": lambda: random.choice([10000, 1000000]),
|
||||
"disable_manual_compaction_one_in": lambda: random.choice([10000, 1000000]),
|
||||
@@ -147,6 +152,7 @@ default_params = {
|
||||
"use_merge": lambda: random.randint(0, 1),
|
||||
# use_put_entity_one_in has to be the same across invocations for verification to work, hence no lambda
|
||||
"use_put_entity_one_in": random.choice([0] * 7 + [1, 5, 10]),
|
||||
"use_attribute_group": lambda: random.randint(0, 1),
|
||||
# 999 -> use Bloom API
|
||||
"bloom_before_level": lambda: random.choice([random.randint(-1, 2), random.randint(-1, 10), 0x7fffffff - 1, 0x7fffffff]),
|
||||
"value_size_mult": 32,
|
||||
@@ -834,9 +840,10 @@ def finalize_and_sanitize(src_params):
|
||||
# verification.
|
||||
dest_params["acquire_snapshot_one_in"] = 0
|
||||
dest_params["compact_range_one_in"] = 0
|
||||
# Give the iterator ops away to reads.
|
||||
dest_params["readpercent"] += dest_params.get("iterpercent", 10)
|
||||
# Redistribute to maintain 100% total
|
||||
dest_params["readpercent"] += dest_params.get("iterpercent", 10) + dest_params.get("prefixpercent", 20)
|
||||
dest_params["iterpercent"] = 0
|
||||
dest_params["prefixpercent"] = 0
|
||||
dest_params["check_multiget_consistency"] = 0
|
||||
dest_params["check_multiget_entity_consistency"] = 0
|
||||
if dest_params.get("disable_wal") == 1:
|
||||
@@ -859,7 +866,6 @@ def finalize_and_sanitize(src_params):
|
||||
dest_params["use_timed_put_one_in"] = 3
|
||||
return dest_params
|
||||
|
||||
|
||||
def gen_cmd_params(args):
|
||||
params = {}
|
||||
|
||||
@@ -1109,13 +1115,13 @@ def whitebox_crash_main(args, unknown_args):
|
||||
}
|
||||
# Single level universal has a lot of special logic. Ensure we cover
|
||||
# it sometimes.
|
||||
if random.randint(0, 1) == 1:
|
||||
if not args.test_tiered_storage and random.randint(0, 1) == 1:
|
||||
additional_opts.update(
|
||||
{
|
||||
"num_levels": 1,
|
||||
}
|
||||
)
|
||||
elif check_mode == 2:
|
||||
elif check_mode == 2 and not args.test_tiered_storage:
|
||||
# normal run with FIFO compaction mode
|
||||
# ops_per_thread is divided by 5 because FIFO compaction
|
||||
# style is quite a bit slower on reads with lot of files
|
||||
|
||||
+121
-22
@@ -107,6 +107,7 @@ const std::string LDBCommand::ARG_PREPOPULATE_BLOB_CACHE =
|
||||
const std::string LDBCommand::ARG_DECODE_BLOB_INDEX = "decode_blob_index";
|
||||
const std::string LDBCommand::ARG_DUMP_UNCOMPRESSED_BLOBS =
|
||||
"dump_uncompressed_blobs";
|
||||
const std::string LDBCommand::ARG_READ_TIMESTAMP = "read_timestamp";
|
||||
|
||||
const char* LDBCommand::DELIM = " ==> ";
|
||||
|
||||
@@ -722,6 +723,46 @@ bool LDBCommand::ParseCompressionTypeOption(
|
||||
return false;
|
||||
}
|
||||
|
||||
Status LDBCommand::MaybePopulateReadTimestamp(ColumnFamilyHandle* cfh,
|
||||
ReadOptions& ropts,
|
||||
Slice* read_timestamp) {
|
||||
const size_t ts_sz = cfh->GetComparator()->timestamp_size();
|
||||
|
||||
auto iter = option_map_.find(ARG_READ_TIMESTAMP);
|
||||
if (iter == option_map_.end()) {
|
||||
if (ts_sz == 0) {
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::InvalidArgument(
|
||||
"column family enables user-defined timestamp while --read_timestamp "
|
||||
"is not provided.");
|
||||
}
|
||||
if (iter->second.empty()) {
|
||||
if (ts_sz == 0) {
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::InvalidArgument(
|
||||
"column family enables user-defined timestamp while --read_timestamp "
|
||||
"is empty.");
|
||||
}
|
||||
if (ts_sz == 0) {
|
||||
return Status::InvalidArgument(
|
||||
"column family does not enable user-defined timestamps while "
|
||||
"--read_timestamp is provided.");
|
||||
}
|
||||
uint64_t int_timestamp;
|
||||
std::istringstream iss(iter->second);
|
||||
if (!(iss >> int_timestamp)) {
|
||||
return Status::InvalidArgument(
|
||||
"column family enables user-defined timestamp while --read_timestamp "
|
||||
"is not a valid uint64 value.");
|
||||
}
|
||||
EncodeU64Ts(int_timestamp, &read_timestamp_);
|
||||
*read_timestamp = read_timestamp_;
|
||||
ropts.timestamp = read_timestamp;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void LDBCommand::OverrideBaseOptions() {
|
||||
options_.create_if_missing = false;
|
||||
|
||||
@@ -769,7 +810,10 @@ void LDBCommand::OverrideBaseCFOptions(ColumnFamilyOptions* cf_opts) {
|
||||
}
|
||||
}
|
||||
|
||||
if (options_.comparator != nullptr) {
|
||||
// Default comparator is BytewiseComparator, so only when it's not, it
|
||||
// means user has a command line override.
|
||||
if (options_.comparator != nullptr &&
|
||||
options_.comparator != BytewiseComparator()) {
|
||||
cf_opts->comparator = options_.comparator;
|
||||
}
|
||||
|
||||
@@ -2846,9 +2890,9 @@ void WALDumperCommand::DoCommand() {
|
||||
GetCommand::GetCommand(const std::vector<std::string>& params,
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags)
|
||||
: LDBCommand(
|
||||
options, flags, true,
|
||||
BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
|
||||
: LDBCommand(options, flags, true,
|
||||
BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX,
|
||||
ARG_VALUE_HEX, ARG_READ_TIMESTAMP})) {
|
||||
if (params.size() != 1) {
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(
|
||||
"<key> must be specified for the get command");
|
||||
@@ -2865,6 +2909,7 @@ void GetCommand::Help(std::string& ret) {
|
||||
ret.append(" ");
|
||||
ret.append(GetCommand::Name());
|
||||
ret.append(" <key>");
|
||||
ret.append(" [--" + ARG_READ_TIMESTAMP + "=<uint64_ts>] ");
|
||||
ret.append(" [--" + ARG_TTL + "]");
|
||||
ret.append("\n");
|
||||
}
|
||||
@@ -2874,8 +2919,18 @@ void GetCommand::DoCommand() {
|
||||
assert(GetExecuteState().IsFailed());
|
||||
return;
|
||||
}
|
||||
ReadOptions ropts;
|
||||
Slice read_timestamp;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
Status st = MaybePopulateReadTimestamp(cfh, ropts, &read_timestamp);
|
||||
if (!st.ok()) {
|
||||
std::stringstream oss;
|
||||
oss << "Get failed: " << st.ToString();
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
|
||||
return;
|
||||
}
|
||||
std::string value;
|
||||
Status st = db_->Get(ReadOptions(), GetCfHandle(), key_, &value);
|
||||
st = db_->Get(ropts, cfh, key_, &value);
|
||||
if (st.ok()) {
|
||||
fprintf(stdout, "%s\n",
|
||||
(is_value_hex_ ? StringToHex(value) : value).c_str());
|
||||
@@ -2895,7 +2950,8 @@ MultiGetCommand::MultiGetCommand(
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags)
|
||||
: LDBCommand(options, flags, true,
|
||||
BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
|
||||
BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX,
|
||||
ARG_READ_TIMESTAMP})) {
|
||||
if (params.size() < 1) {
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(
|
||||
"At least one <key> must be specified for multi_get.");
|
||||
@@ -2911,6 +2967,7 @@ void MultiGetCommand::Help(std::string& ret) {
|
||||
ret.append(" ");
|
||||
ret.append(MultiGetCommand::Name());
|
||||
ret.append(" <key_1> <key_2> <key_3> ...");
|
||||
ret.append(" [--" + ARG_READ_TIMESTAMP + "=<uint64_ts>] ");
|
||||
ret.append("\n");
|
||||
}
|
||||
|
||||
@@ -2919,6 +2976,16 @@ void MultiGetCommand::DoCommand() {
|
||||
assert(GetExecuteState().IsFailed());
|
||||
return;
|
||||
}
|
||||
ReadOptions ropts;
|
||||
Slice read_timestamp;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
Status st = MaybePopulateReadTimestamp(cfh, ropts, &read_timestamp);
|
||||
if (!st.ok()) {
|
||||
std::stringstream oss;
|
||||
oss << "MultiGet failed: " << st.ToString();
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
|
||||
return;
|
||||
}
|
||||
size_t num_keys = keys_.size();
|
||||
std::vector<Slice> key_slices;
|
||||
std::vector<PinnableSlice> values(num_keys);
|
||||
@@ -2926,8 +2993,8 @@ void MultiGetCommand::DoCommand() {
|
||||
for (const std::string& key : keys_) {
|
||||
key_slices.emplace_back(key);
|
||||
}
|
||||
db_->MultiGet(ReadOptions(), GetCfHandle(), num_keys, key_slices.data(),
|
||||
values.data(), statuses.data());
|
||||
db_->MultiGet(ropts, cfh, num_keys, key_slices.data(), values.data(),
|
||||
statuses.data());
|
||||
|
||||
bool failed = false;
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
@@ -2957,9 +3024,9 @@ GetEntityCommand::GetEntityCommand(
|
||||
const std::vector<std::string>& params,
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags)
|
||||
: LDBCommand(
|
||||
options, flags, true,
|
||||
BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
|
||||
: LDBCommand(options, flags, true,
|
||||
BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX,
|
||||
ARG_VALUE_HEX, ARG_READ_TIMESTAMP})) {
|
||||
if (params.size() != 1) {
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(
|
||||
"<key> must be specified for the get_entity command");
|
||||
@@ -2976,6 +3043,7 @@ void GetEntityCommand::Help(std::string& ret) {
|
||||
ret.append(" ");
|
||||
ret.append(GetEntityCommand::Name());
|
||||
ret.append(" <key>");
|
||||
ret.append(" [--" + ARG_READ_TIMESTAMP + "=<uint64_ts>] ");
|
||||
ret.append(" [--" + ARG_TTL + "]");
|
||||
ret.append("\n");
|
||||
}
|
||||
@@ -2985,9 +3053,18 @@ void GetEntityCommand::DoCommand() {
|
||||
assert(GetExecuteState().IsFailed());
|
||||
return;
|
||||
}
|
||||
ReadOptions ropt;
|
||||
Slice read_timestamp;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
Status st = MaybePopulateReadTimestamp(cfh, ropt, &read_timestamp);
|
||||
if (!st.ok()) {
|
||||
std::stringstream oss;
|
||||
oss << "GetEntity failed: " << st.ToString();
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
|
||||
return;
|
||||
}
|
||||
PinnableWideColumns pinnable_wide_columns;
|
||||
Status st = db_->GetEntity(ReadOptions(), GetCfHandle(), key_,
|
||||
&pinnable_wide_columns);
|
||||
st = db_->GetEntity(ropt, cfh, key_, &pinnable_wide_columns);
|
||||
if (st.ok()) {
|
||||
std::ostringstream oss;
|
||||
WideColumnsHelper::DumpWideColumns(pinnable_wide_columns.columns(), oss,
|
||||
@@ -3007,7 +3084,8 @@ MultiGetEntityCommand::MultiGetEntityCommand(
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags)
|
||||
: LDBCommand(options, flags, true /* is_read_only */,
|
||||
BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
|
||||
BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX,
|
||||
ARG_READ_TIMESTAMP})) {
|
||||
if (params.size() < 1) {
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(
|
||||
"At least one <key> must be specified for the multi_get_entity "
|
||||
@@ -3024,6 +3102,7 @@ void MultiGetEntityCommand::Help(std::string& ret) {
|
||||
ret.append(" ");
|
||||
ret.append(MultiGetEntityCommand::Name());
|
||||
ret.append(" <key_1> <key_2> <key_3> ...");
|
||||
ret.append(" [--" + ARG_READ_TIMESTAMP + "=<uint64_ts>] ");
|
||||
ret.append("\n");
|
||||
}
|
||||
|
||||
@@ -3033,6 +3112,16 @@ void MultiGetEntityCommand::DoCommand() {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOptions ropt;
|
||||
Slice read_timestamp;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
Status st = MaybePopulateReadTimestamp(cfh, ropt, &read_timestamp);
|
||||
if (!st.ok()) {
|
||||
std::stringstream oss;
|
||||
oss << "MultiGetEntity failed: " << st.ToString();
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
|
||||
return;
|
||||
}
|
||||
size_t num_keys = keys_.size();
|
||||
std::vector<Slice> key_slices;
|
||||
std::vector<PinnableWideColumns> results(num_keys);
|
||||
@@ -3041,8 +3130,8 @@ void MultiGetEntityCommand::DoCommand() {
|
||||
key_slices.emplace_back(key);
|
||||
}
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), GetCfHandle(), num_keys, key_slices.data(),
|
||||
results.data(), statuses.data());
|
||||
db_->MultiGetEntity(ropt, cfh, num_keys, key_slices.data(), results.data(),
|
||||
statuses.data());
|
||||
|
||||
bool failed = false;
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
@@ -3200,11 +3289,11 @@ void BatchPutCommand::OverrideBaseOptions() {
|
||||
ScanCommand::ScanCommand(const std::vector<std::string>& /*params*/,
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags)
|
||||
: LDBCommand(
|
||||
options, flags, true,
|
||||
BuildCmdLineOptions({ARG_TTL, ARG_NO_VALUE, ARG_HEX, ARG_KEY_HEX,
|
||||
ARG_TO, ARG_VALUE_HEX, ARG_FROM, ARG_TIMESTAMP,
|
||||
ARG_MAX_KEYS, ARG_TTL_START, ARG_TTL_END})),
|
||||
: LDBCommand(options, flags, true,
|
||||
BuildCmdLineOptions(
|
||||
{ARG_TTL, ARG_NO_VALUE, ARG_HEX, ARG_KEY_HEX, ARG_TO,
|
||||
ARG_VALUE_HEX, ARG_FROM, ARG_TIMESTAMP, ARG_MAX_KEYS,
|
||||
ARG_TTL_START, ARG_TTL_END, ARG_READ_TIMESTAMP})),
|
||||
start_key_specified_(false),
|
||||
end_key_specified_(false),
|
||||
max_keys_scanned_(-1),
|
||||
@@ -3260,6 +3349,7 @@ void ScanCommand::Help(std::string& ret) {
|
||||
ret.append(" [--" + ARG_TTL_START + "=<N>:- is inclusive]");
|
||||
ret.append(" [--" + ARG_TTL_END + "=<N>:- is exclusive]");
|
||||
ret.append(" [--" + ARG_NO_VALUE + "]");
|
||||
ret.append(" [--" + ARG_READ_TIMESTAMP + "=<uint64_ts>] ");
|
||||
ret.append("\n");
|
||||
}
|
||||
|
||||
@@ -3271,8 +3361,17 @@ void ScanCommand::DoCommand() {
|
||||
|
||||
int num_keys_scanned = 0;
|
||||
ReadOptions scan_read_opts;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
Slice read_timestamp;
|
||||
Status st = MaybePopulateReadTimestamp(cfh, scan_read_opts, &read_timestamp);
|
||||
if (!st.ok()) {
|
||||
std::stringstream oss;
|
||||
oss << "Scan failed: " << st.ToString();
|
||||
exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
|
||||
return;
|
||||
}
|
||||
scan_read_opts.total_order_seek = true;
|
||||
Iterator* it = db_->NewIterator(scan_read_opts, GetCfHandle());
|
||||
Iterator* it = db_->NewIterator(scan_read_opts, cfh);
|
||||
if (start_key_specified_) {
|
||||
it->Seek(start_key_);
|
||||
} else {
|
||||
|
||||
@@ -85,6 +85,9 @@ void LDBCommandRunner::PrintHelp(const LDBOptions& ldb_options,
|
||||
"=<double,e.g.:0.25>\n");
|
||||
ret.append(" --" + LDBCommand::ARG_BLOB_COMPACTION_READAHEAD_SIZE +
|
||||
"=<int,e.g.:2097152>\n");
|
||||
ret.append(" --" + LDBCommand::ARG_READ_TIMESTAMP +
|
||||
"=<uint64_ts, e.g.:323> : read timestamp, required if column "
|
||||
"family enables timestamp, otherwise invalid if provided.");
|
||||
|
||||
ret.append("\n\n");
|
||||
ret.append("Data Access Commands:\n");
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* Enabling `BlockBasedTableOptions::block_align` is now incompatible (i.e., APIs will return `Status::InvalidArgument`) with more ways of enabling compression: `CompactionOptions::compression`, `ColumnFamilyOptions::compression_per_level`, and `ColumnFamilyOptions::bottommost_compression`.
|
||||
@@ -1 +0,0 @@
|
||||
* Changed the default value of `CompactionOptions::compression` to `kDisableCompressionOption`, which means the compression type is determined by the `ColumnFamilyOptions`.
|
||||
@@ -1 +0,0 @@
|
||||
`BlockBasedTableOptions::optimize_filters_for_memory` is now set to true by default. When `partition_filters=false`, this could lead to somewhat increased average RSS memory usage by the block cache, but this "extra" usage is within the allowed memory budget and should make memory usage more consistent (by minimizing internal fragmentation for more kinds of blocks).
|
||||
@@ -1 +0,0 @@
|
||||
Dump all keys for cache dumper impl if `SetDumpFilter()` is not called
|
||||
@@ -1 +0,0 @@
|
||||
`CompactRange()` with `CompactRangeOptions::change_level = true` and `CompactRangeOptions::target_level = 0` that ends up moving more than 1 file from non-L0 to L0 will return `Status::Aborted()`.
|
||||
@@ -1 +0,0 @@
|
||||
On distributed file systems that support file system level checksum verification and reconstruction reads, RocksDB will now retry a file read if the initial read fails RocksDB block level or record level checksum verification. This applies to MANIFEST file reads when the DB is opened, and to SST file reads at all times.
|
||||
@@ -1 +0,0 @@
|
||||
Fix a bug causing `VerifyFileChecksums()` to return false-positive corruption under `BlockBasedTableOptions::block_align=true`
|
||||
@@ -1 +0,0 @@
|
||||
Provide consistent view of the database across the column families for `NewIterators()` API.
|
||||
-1
@@ -1 +0,0 @@
|
||||
* Fixed feature interaction bug for `DeleteRange()` together with `ColumnFamilyOptions::memtable_insert_with_hint_prefix_extractor`. The impact of this bug would likely be corruption or crashing.
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed hang in `DisableManualCompactions()` where compactions waiting to be scheduled due to conflicts would not be canceled promptly
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed a regression when `ColumnFamilyOptions::max_successive_merges > 0` where the CPU overhead for deciding whether to merge could have increased unless the user had set the option `ColumnFamilyOptions::strict_max_successive_merges`
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed a bug in `MultiGet()` and `MultiGetEntity()` together with blob files (`ColumnFamilyOptions::enable_blob_files == true`). An error looking up one of the keys could cause the results to be wrong for other keys for which the statuses were `Status::OK`.
|
||||
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user