mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Compare commits
22 Commits
f2c0eb41ef
...
7.8.fb
| Author | SHA1 | Date | |
|---|---|---|---|
| bf2c335184 | |||
| 7aa573eb45 | |||
| e0cf5cd5f3 | |||
| 7ad900e1fb | |||
| 491615ccaa | |||
| 1cf453943d | |||
| 442b6b8048 | |||
| 8c06988313 | |||
| 8d5edb693c | |||
| 32d853f91c | |||
| e0b1793c2f | |||
| f690f2449a | |||
| c097b01467 | |||
| 6b2f41f223 | |||
| d53da91ec2 | |||
| 0ffd94d615 | |||
| fb7b420443 | |||
| 3453a88b57 | |||
| 49b7f219de | |||
| 0d82c62664 | |||
| 3ecef275be | |||
| 9e0c4a0aa7 |
@@ -584,7 +584,7 @@ jobs:
|
||||
name: "Test RocksDB"
|
||||
shell: powershell.exe
|
||||
command: |
|
||||
build_tools\run_ci_db_test.ps1 -SuiteRun arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test -Concurrency 16
|
||||
build_tools\run_ci_db_test.ps1 -SuiteRun db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test -Concurrency 16
|
||||
|
||||
build-linux-java:
|
||||
executor: linux-docker
|
||||
|
||||
@@ -796,7 +796,6 @@ set(SOURCES
|
||||
options/options.cc
|
||||
options/options_helper.cc
|
||||
options/options_parser.cc
|
||||
port/mmap.cc
|
||||
port/stack_trace.cc
|
||||
table/adaptive/adaptive_table_factory.cc
|
||||
table/block_based/binary_search_index_reader.cc
|
||||
|
||||
+18
@@ -1,4 +1,21 @@
|
||||
# Rocksdb Change Log
|
||||
## 7.8.3 (11/29/2022)
|
||||
* Revert an internal change in 7.8.0 associated with some memory usage churn.
|
||||
|
||||
## 7.8.2 (11/27/2022)
|
||||
### Behavior changes
|
||||
* Make best-efforts recovery verify SST unique ID before Version construction (#10962)
|
||||
* Fix failed memtable flush retry bug that could cause wrongly ordered updates, which would surface to writers as `Status::Corruption` in case of `force_consistency_checks=true` (default). It affects use cases that enable both parallel flush (`max_background_flushes > 1` or `max_background_jobs >= 8`) and non-default memtable count (`max_write_buffer_number > 2`).
|
||||
* Tiered Storage: fixed excessive keys written to penultimate level in non-debug builds.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a regression in scan for async_io. During seek, valid buffers were getting cleared causing a regression.
|
||||
* Fixed a performance regression in iterator where range tombstones after `iterate_upper_bound` is processed.
|
||||
|
||||
## 7.8.1 (11/2/2022)
|
||||
### Bug Fixes
|
||||
* Fix memory corruption error in scans if async_io is enabled. Memory corruption happened if there is IOError while reading the data leading to empty buffer and other buffer already in progress of async read goes again for reading.
|
||||
|
||||
## 7.8.0 (10/22/2022)
|
||||
### New Features
|
||||
* `DeleteRange()` now supports user-defined timestamp.
|
||||
@@ -28,6 +45,7 @@
|
||||
* Try to align the compaction output file boundaries to the next level ones, which can reduce more than 10% compaction load for the default level compaction. The feature is enabled by default, to disable, set `AdvancedColumnFamilyOptions.level_compaction_dynamic_file_size` to false. As a side effect, it can create SSTs larger than the target_file_size (capped at 2x target_file_size) or smaller files.
|
||||
* Improve RoundRobin TTL compaction, which is going to be the same as normal RoundRobin compaction to move the compaction cursor.
|
||||
* Fix a small CPU regression caused by a change that UserComparatorWrapper was made Customizable, because Customizable itself has small CPU overhead for initialization.
|
||||
* Fixed an iterator performance regression for delete range users when scanning through a consecutive sequence of range tombstones (#10877).
|
||||
|
||||
### Behavior Changes
|
||||
* Sanitize min_write_buffer_number_to_merge to 1 if atomic flush is enabled to prevent unexpected data loss when WAL is disabled in a multi-column-family setting (#10773).
|
||||
|
||||
@@ -163,7 +163,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"options/options.cc",
|
||||
"options/options_helper.cc",
|
||||
"options/options_parser.cc",
|
||||
"port/mmap.cc",
|
||||
"port/port_posix.cc",
|
||||
"port/stack_trace.cc",
|
||||
"port/win/env_default.cc",
|
||||
@@ -503,7 +502,6 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"options/options.cc",
|
||||
"options/options_helper.cc",
|
||||
"options/options_parser.cc",
|
||||
"port/mmap.cc",
|
||||
"port/port_posix.cc",
|
||||
"port/stack_trace.cc",
|
||||
"port/win/env_default.cc",
|
||||
|
||||
Vendored
+4
-5
@@ -386,7 +386,7 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, LRUHandle** handle,
|
||||
last_reference_list.push_back(e);
|
||||
} else {
|
||||
if (free_handle_on_fail) {
|
||||
delete[] reinterpret_cast<char*>(e);
|
||||
free(e);
|
||||
*handle = nullptr;
|
||||
}
|
||||
s = Status::MemoryLimit("Insert failed due to LRU cache being full.");
|
||||
@@ -559,8 +559,7 @@ LRUHandle* LRUCacheShard::Lookup(const Slice& key, uint32_t hash,
|
||||
secondary_cache_->Lookup(key, create_cb, wait, found_dummy_entry,
|
||||
is_in_sec_cache);
|
||||
if (secondary_handle != nullptr) {
|
||||
e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + key.size()]);
|
||||
e = static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->m_flags = 0;
|
||||
e->im_flags = 0;
|
||||
@@ -683,8 +682,8 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
// Allocate the memory here outside of the mutex.
|
||||
// If the cache is full, we'll have to release it.
|
||||
// It shouldn't happen very often though.
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + key.size()]);
|
||||
LRUHandle* e =
|
||||
static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->value = value;
|
||||
e->m_flags = 0;
|
||||
|
||||
Vendored
+3
-1
@@ -212,6 +212,7 @@ struct LRUHandle {
|
||||
|
||||
void Free() {
|
||||
assert(refs == 0);
|
||||
|
||||
if (!IsSecondaryCacheCompatible() && info_.deleter) {
|
||||
(*info_.deleter)(key(), value);
|
||||
} else if (IsSecondaryCacheCompatible()) {
|
||||
@@ -226,7 +227,8 @@ struct LRUHandle {
|
||||
(*info_.helper->del_cb)(key(), value);
|
||||
}
|
||||
}
|
||||
delete[] reinterpret_cast<char*>(this);
|
||||
|
||||
free(this);
|
||||
}
|
||||
|
||||
inline size_t CalcuMetaCharge(
|
||||
|
||||
@@ -1100,7 +1100,9 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput.context",
|
||||
&context);
|
||||
output_to_penultimate_level_ = context.output_to_penultimate_level;
|
||||
#endif /* !NDEBUG */
|
||||
#else
|
||||
output_to_penultimate_level_ = false;
|
||||
#endif // NDEBUG
|
||||
|
||||
// if the key is newer than the cutoff sequence or within the earliest
|
||||
// snapshot, it should output to the penultimate level.
|
||||
|
||||
@@ -1819,7 +1819,8 @@ InternalIterator* DBImpl::NewInternalIterator(
|
||||
MergeIteratorBuilder merge_iter_builder(
|
||||
&cfd->internal_comparator(), arena,
|
||||
!read_options.total_order_seek &&
|
||||
super_version->mutable_cf_options.prefix_extractor != nullptr);
|
||||
super_version->mutable_cf_options.prefix_extractor != nullptr,
|
||||
read_options.iterate_upper_bound);
|
||||
// Collect iterator for mutable memtable
|
||||
auto mem_iter = super_version->mem->NewIterator(read_options, arena);
|
||||
Status s;
|
||||
|
||||
@@ -2754,6 +2754,46 @@ TEST_F(DBRangeDelTest, RefreshMemtableIter) {
|
||||
ASSERT_OK(iter->Refresh());
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, RangeTombstoneRespectIterateUpperBound) {
|
||||
// Memtable: a, [b, bz)
|
||||
// Do a Seek on `a` with iterate_upper_bound being az
|
||||
// range tombstone [b, bz) should not be processed (added to and
|
||||
// popped from the min_heap in MergingIterator).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "bar"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "b", "bz"));
|
||||
|
||||
// I could not find a cleaner way to test this without relying on
|
||||
// implementation detail. Tried to test the value of
|
||||
// `internal_range_del_reseek_count` but that did not work
|
||||
// since BlockBasedTable iterator becomes !Valid() when point key
|
||||
// is out of bound and that reseek only happens when a point key
|
||||
// is covered by some range tombstone.
|
||||
SyncPoint::GetInstance()->SetCallBack("MergeIterator::PopDeleteRangeStart",
|
||||
[](void*) {
|
||||
// there should not be any range
|
||||
// tombstone in the heap.
|
||||
FAIL();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string upper_bound = "az";
|
||||
Slice upper_bound_slice = upper_bound;
|
||||
read_opts.iterate_upper_bound = &upper_bound_slice;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(read_opts)};
|
||||
iter->Seek("a");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), "a");
|
||||
iter->Next();
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -7506,6 +7506,80 @@ TEST_F(DBTest2, SstUniqueIdVerifyMultiCFs) {
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, BestEffortsRecoveryWithSstUniqueIdVerification) {
|
||||
const auto tamper_with_uniq_id = [&](void* arg) {
|
||||
auto props = static_cast<TableProperties*>(arg);
|
||||
assert(props);
|
||||
// update table property session_id to a different one
|
||||
props->db_session_id = DBImpl::GenerateDbSessionId(nullptr);
|
||||
};
|
||||
|
||||
const auto assert_db = [&](size_t expected_count,
|
||||
const std::string& expected_v) {
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(ReadOptions()));
|
||||
size_t cnt = 0;
|
||||
for (it->SeekToFirst(); it->Valid(); it->Next(), ++cnt) {
|
||||
ASSERT_EQ(std::to_string(cnt), it->key());
|
||||
ASSERT_EQ(expected_v, it->value());
|
||||
}
|
||||
ASSERT_EQ(expected_count, cnt);
|
||||
};
|
||||
|
||||
const int num_l0_compaction_trigger = 8;
|
||||
const int num_l0 = num_l0_compaction_trigger - 1;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = num_l0_compaction_trigger;
|
||||
|
||||
for (int k = 0; k < num_l0; ++k) {
|
||||
// Allow mismatch for now
|
||||
options.verify_sst_unique_id_in_manifest = false;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t num_keys_per_file = 10;
|
||||
for (int i = 0; i < num_l0; ++i) {
|
||||
for (size_t j = 0; j < num_keys_per_file; ++j) {
|
||||
ASSERT_OK(Put(std::to_string(j), "v" + std::to_string(i)));
|
||||
}
|
||||
if (i == k) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PropertyBlockBuilder::AddTableProperty:Start",
|
||||
tamper_with_uniq_id);
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
|
||||
options.best_efforts_recovery = true;
|
||||
Reopen(options);
|
||||
assert_db(k == 0 ? 0 : num_keys_per_file, "v" + std::to_string(k - 1));
|
||||
|
||||
// Reopen with regular recovery
|
||||
options.best_efforts_recovery = false;
|
||||
Reopen(options);
|
||||
assert_db(k == 0 ? 0 : num_keys_per_file, "v" + std::to_string(k - 1));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
for (size_t i = 0; i < num_keys_per_file; ++i) {
|
||||
ASSERT_OK(Put(std::to_string(i), "v"));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
Reopen(options);
|
||||
{
|
||||
for (size_t i = 0; i < num_keys_per_file; ++i) {
|
||||
ASSERT_EQ("v", Get(std::to_string(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
|
||||
Destroy(last_options_);
|
||||
|
||||
@@ -49,9 +49,6 @@
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/merge_operators.h"
|
||||
|
||||
// In case defined by Windows headers
|
||||
#undef small
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class MockEnv;
|
||||
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ MemTable::MemTable(const InternalKeyComparator& cmp,
|
||||
: comparator_(cmp),
|
||||
moptions_(ioptions, mutable_cf_options),
|
||||
refs_(0),
|
||||
kArenaBlockSize(Arena::OptimizeBlockSize(moptions_.arena_block_size)),
|
||||
kArenaBlockSize(OptimizeBlockSize(moptions_.arena_block_size)),
|
||||
mem_tracker_(write_buffer_manager),
|
||||
arena_(moptions_.arena_block_size,
|
||||
(write_buffer_manager != nullptr &&
|
||||
|
||||
@@ -419,6 +419,13 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
std::max(m->GetNextLogNumber(), *max_next_log_number);
|
||||
}
|
||||
ret->push_back(m);
|
||||
} else if (!ret->empty()) {
|
||||
// This `break` is necessary to prevent picking non-consecutive memtables
|
||||
// in case `memlist` has one or more entries with
|
||||
// `flush_in_progress_ == true` sandwiched between entries with
|
||||
// `flush_in_progress_ == false`. This could happen after parallel flushes
|
||||
// are picked and the one flushing older memtables is rolled back.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!atomic_flush || num_flush_not_started_ == 0) {
|
||||
|
||||
+35
-13
@@ -742,28 +742,40 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
// Pick tables to flush
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush);
|
||||
// Should pick 4 of 5 since 1 table has been picked in to_flush2
|
||||
ASSERT_EQ(4, to_flush.size());
|
||||
// Picks three oldest memtables. The fourth oldest is picked in `to_flush2` so
|
||||
// must be excluded. The newest (fifth oldest) is non-consecutive with the
|
||||
// three oldest due to omitting the fourth oldest so must not be picked.
|
||||
ASSERT_EQ(3, to_flush.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Pick tables to flush again
|
||||
autovector<MemTable*> to_flush3;
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush3);
|
||||
ASSERT_EQ(0, to_flush3.size()); // nothing not in progress of being flushed
|
||||
// Picks newest (fifth oldest)
|
||||
ASSERT_EQ(1, to_flush3.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 4 memtables that were picked in to_flush
|
||||
// Nothing left to flush
|
||||
autovector<MemTable*> to_flush4;
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush4);
|
||||
ASSERT_EQ(0, to_flush4.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 3 memtables that were picked in to_flush
|
||||
s = Mock_InstallMemtableFlushResults(&list, mutable_cf_options, to_flush,
|
||||
&to_delete);
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Note: now to_flush contains tables[0,1,2,4]. to_flush2 contains
|
||||
// tables[3].
|
||||
// Note: now to_flush contains tables[0,1,2]. to_flush2 contains
|
||||
// tables[3]. to_flush3 contains tables[4].
|
||||
// Current implementation will only commit memtables in the order they were
|
||||
// created. So TryInstallMemtableFlushResults will install the first 3 tables
|
||||
// in to_flush and stop when it encounters a table not yet flushed.
|
||||
@@ -779,7 +791,17 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 1 memtable that was picked in to_flush2
|
||||
// Flush the 1 memtable (tables[4]) that was picked in to_flush3
|
||||
s = MemTableListTest::Mock_InstallMemtableFlushResults(
|
||||
&list, mutable_cf_options, to_flush3, &to_delete);
|
||||
ASSERT_OK(s);
|
||||
|
||||
// This will install 0 tables since tables[4] flushed while tables[3] has not
|
||||
// yet flushed.
|
||||
ASSERT_EQ(2, list.NumNotFlushed());
|
||||
ASSERT_EQ(0, to_delete.size());
|
||||
|
||||
// Flush the 1 memtable (tables[3]) that was picked in to_flush2
|
||||
s = MemTableListTest::Mock_InstallMemtableFlushResults(
|
||||
&list, mutable_cf_options, to_flush2, &to_delete);
|
||||
ASSERT_OK(s);
|
||||
@@ -809,11 +831,11 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
memtable_id = 4;
|
||||
// Pick tables to flush. The tables to pick must have ID smaller than or
|
||||
// equal to 4. Therefore, no table will be selected in this case.
|
||||
autovector<MemTable*> to_flush4;
|
||||
autovector<MemTable*> to_flush5;
|
||||
list.FlushRequested();
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush4);
|
||||
ASSERT_TRUE(to_flush4.empty());
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush5);
|
||||
ASSERT_TRUE(to_flush5.empty());
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
@@ -823,8 +845,8 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
// equal to 5. Therefore, only tables[5] will be selected.
|
||||
memtable_id = 5;
|
||||
list.FlushRequested();
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush4);
|
||||
ASSERT_EQ(1, static_cast<int>(to_flush4.size()));
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush5);
|
||||
ASSERT_EQ(1, static_cast<int>(to_flush5.size()));
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
|
||||
@@ -734,12 +734,13 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersion(
|
||||
assert(!cfd->ioptions()->cf_paths.empty());
|
||||
Status s;
|
||||
for (const auto& elem : edit.GetNewFiles()) {
|
||||
int level = elem.first;
|
||||
const FileMetaData& meta = elem.second;
|
||||
const FileDescriptor& fd = meta.fd;
|
||||
uint64_t file_num = fd.GetNumber();
|
||||
const std::string fpath =
|
||||
MakeTableFileName(cfd->ioptions()->cf_paths[0].path, file_num);
|
||||
s = VerifyFile(fpath, meta);
|
||||
s = VerifyFile(cfd, fpath, level, meta);
|
||||
if (s.IsPathNotFound() || s.IsNotFound() || s.IsCorruption()) {
|
||||
missing_files.insert(file_num);
|
||||
s = Status::OK();
|
||||
@@ -804,6 +805,18 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersion(
|
||||
auto* version = new Version(cfd, version_set_, version_set_->file_options_,
|
||||
*cfd->GetLatestMutableCFOptions(), io_tracer_,
|
||||
version_set_->current_version_number_++);
|
||||
s = builder->LoadTableHandlers(
|
||||
cfd->internal_stats(),
|
||||
version_set_->db_options_->max_file_opening_threads, false, true,
|
||||
cfd->GetLatestMutableCFOptions()->prefix_extractor,
|
||||
MaxFileSizeForL0MetaPin(*cfd->GetLatestMutableCFOptions()));
|
||||
if (!s.ok()) {
|
||||
delete version;
|
||||
if (s.IsCorruption()) {
|
||||
s = Status::OK();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
s = builder->SaveTo(version->storage_info());
|
||||
if (s.ok()) {
|
||||
version->PrepareAppend(
|
||||
@@ -823,9 +836,11 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersion(
|
||||
return s;
|
||||
}
|
||||
|
||||
Status VersionEditHandlerPointInTime::VerifyFile(const std::string& fpath,
|
||||
Status VersionEditHandlerPointInTime::VerifyFile(ColumnFamilyData* cfd,
|
||||
const std::string& fpath,
|
||||
int level,
|
||||
const FileMetaData& fmeta) {
|
||||
return version_set_->VerifyFileMetadata(fpath, fmeta);
|
||||
return version_set_->VerifyFileMetadata(cfd, fpath, level, fmeta);
|
||||
}
|
||||
|
||||
Status VersionEditHandlerPointInTime::VerifyBlobFile(
|
||||
@@ -843,6 +858,12 @@ Status VersionEditHandlerPointInTime::VerifyBlobFile(
|
||||
return s;
|
||||
}
|
||||
|
||||
Status VersionEditHandlerPointInTime::LoadTables(
|
||||
ColumnFamilyData* /*cfd*/, bool /*prefetch_index_and_filter_in_cache*/,
|
||||
bool /*is_initial_load*/) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ManifestTailer::Initialize() {
|
||||
if (Mode::kRecovery == mode_) {
|
||||
return VersionEditHandler::Initialize();
|
||||
@@ -930,9 +951,11 @@ void ManifestTailer::CheckIterationResult(const log::Reader& reader,
|
||||
}
|
||||
}
|
||||
|
||||
Status ManifestTailer::VerifyFile(const std::string& fpath,
|
||||
Status ManifestTailer::VerifyFile(ColumnFamilyData* cfd,
|
||||
const std::string& fpath, int level,
|
||||
const FileMetaData& fmeta) {
|
||||
Status s = VersionEditHandlerPointInTime::VerifyFile(fpath, fmeta);
|
||||
Status s =
|
||||
VersionEditHandlerPointInTime::VerifyFile(cfd, fpath, level, fmeta);
|
||||
// TODO: Open file or create hard link to prevent the file from being
|
||||
// deleted.
|
||||
return s;
|
||||
|
||||
@@ -164,7 +164,7 @@ class VersionEditHandler : public VersionEditHandlerBase {
|
||||
ColumnFamilyData* cfd,
|
||||
bool force_create_version);
|
||||
|
||||
Status LoadTables(ColumnFamilyData* cfd,
|
||||
virtual Status LoadTables(ColumnFamilyData* cfd,
|
||||
bool prefetch_index_and_filter_in_cache,
|
||||
bool is_initial_load);
|
||||
|
||||
@@ -213,11 +213,15 @@ class VersionEditHandlerPointInTime : public VersionEditHandler {
|
||||
ColumnFamilyData* DestroyCfAndCleanup(const VersionEdit& edit) override;
|
||||
Status MaybeCreateVersion(const VersionEdit& edit, ColumnFamilyData* cfd,
|
||||
bool force_create_version) override;
|
||||
virtual Status VerifyFile(const std::string& fpath,
|
||||
const FileMetaData& fmeta);
|
||||
virtual Status VerifyFile(ColumnFamilyData* cfd, const std::string& fpath,
|
||||
int level, const FileMetaData& fmeta);
|
||||
virtual Status VerifyBlobFile(ColumnFamilyData* cfd, uint64_t blob_file_num,
|
||||
const BlobFileAddition& blob_addition);
|
||||
|
||||
Status LoadTables(ColumnFamilyData* cfd,
|
||||
bool prefetch_index_and_filter_in_cache,
|
||||
bool is_initial_load) override;
|
||||
|
||||
std::unordered_map<uint32_t, Version*> versions_;
|
||||
};
|
||||
|
||||
@@ -250,7 +254,7 @@ class ManifestTailer : public VersionEditHandlerPointInTime {
|
||||
|
||||
void CheckIterationResult(const log::Reader& reader, Status* s) override;
|
||||
|
||||
Status VerifyFile(const std::string& fpath,
|
||||
Status VerifyFile(ColumnFamilyData* cfd, const std::string& fpath, int level,
|
||||
const FileMetaData& fmeta) override;
|
||||
|
||||
enum Mode : uint8_t {
|
||||
|
||||
+36
-2
@@ -2833,6 +2833,7 @@ Status Version::MultiGetAsync(
|
||||
std::vector<Status> statuses = folly::coro::blockingWait(
|
||||
folly::coro::collectAllRange(std::move(mget_tasks))
|
||||
.scheduleOn(&range->context()->executor()));
|
||||
mget_tasks.clear();
|
||||
for (Status stat : statuses) {
|
||||
if (!stat.ok()) {
|
||||
s = stat;
|
||||
@@ -6723,8 +6724,9 @@ uint64_t VersionSet::GetTotalBlobFileSize(Version* dummy_versions) {
|
||||
return all_versions_blob_file_size;
|
||||
}
|
||||
|
||||
Status VersionSet::VerifyFileMetadata(const std::string& fpath,
|
||||
const FileMetaData& meta) const {
|
||||
Status VersionSet::VerifyFileMetadata(ColumnFamilyData* cfd,
|
||||
const std::string& fpath, int level,
|
||||
const FileMetaData& meta) {
|
||||
uint64_t fsize = 0;
|
||||
Status status = fs_->GetFileSize(fpath, IOOptions(), &fsize, nullptr);
|
||||
if (status.ok()) {
|
||||
@@ -6732,6 +6734,38 @@ Status VersionSet::VerifyFileMetadata(const std::string& fpath,
|
||||
status = Status::Corruption("File size mismatch: " + fpath);
|
||||
}
|
||||
}
|
||||
if (status.ok() && db_options_->verify_sst_unique_id_in_manifest) {
|
||||
assert(cfd);
|
||||
TableCache* table_cache = cfd->table_cache();
|
||||
assert(table_cache);
|
||||
|
||||
const MutableCFOptions* const cf_opts = cfd->GetLatestMutableCFOptions();
|
||||
assert(cf_opts);
|
||||
std::shared_ptr<const SliceTransform> pe = cf_opts->prefix_extractor;
|
||||
size_t max_sz_for_l0_meta_pin = MaxFileSizeForL0MetaPin(*cf_opts);
|
||||
|
||||
const FileOptions& file_opts = file_options();
|
||||
|
||||
Version* version = cfd->current();
|
||||
assert(version);
|
||||
VersionStorageInfo& storage_info = version->storage_info_;
|
||||
const InternalKeyComparator* icmp = storage_info.InternalComparator();
|
||||
assert(icmp);
|
||||
|
||||
InternalStats* internal_stats = cfd->internal_stats();
|
||||
|
||||
FileMetaData meta_copy = meta;
|
||||
status = table_cache->FindTable(
|
||||
ReadOptions(), file_opts, *icmp, meta_copy,
|
||||
&(meta_copy.table_reader_handle), pe,
|
||||
/*no_io=*/false, /*record_read_stats=*/true,
|
||||
internal_stats->GetFileReadHist(level), false, level,
|
||||
/*prefetch_index_and_filter_in_cache*/ false, max_sz_for_l0_meta_pin,
|
||||
meta_copy.temperature);
|
||||
if (meta_copy.table_reader_handle) {
|
||||
table_cache->ReleaseHandle(meta_copy.table_reader_handle);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1503,8 +1503,8 @@ class VersionSet {
|
||||
ColumnFamilyData* CreateColumnFamily(const ColumnFamilyOptions& cf_options,
|
||||
const VersionEdit* edit);
|
||||
|
||||
Status VerifyFileMetadata(const std::string& fpath,
|
||||
const FileMetaData& meta) const;
|
||||
Status VerifyFileMetadata(ColumnFamilyData* cfd, const std::string& fpath,
|
||||
int level, const FileMetaData& meta);
|
||||
|
||||
// Protected by DB mutex.
|
||||
WalSet wals_;
|
||||
|
||||
+133
-64
@@ -225,10 +225,8 @@ void FilePrefetchBuffer::AbortIOIfNeeded(uint64_t offset) {
|
||||
bufs_[second].async_read_in_progress_ = false;
|
||||
}
|
||||
|
||||
if (bufs_[curr_].io_handle_ == nullptr &&
|
||||
bufs_[curr_].async_read_in_progress_) {
|
||||
if (bufs_[curr_].io_handle_ == nullptr) {
|
||||
bufs_[curr_].async_read_in_progress_ = false;
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,17 +266,37 @@ void FilePrefetchBuffer::UpdateBuffersIfNeeded(uint64_t offset) {
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
|
||||
// If data starts from second buffer, make it curr_. Second buffer can be
|
||||
// either partial filled or full.
|
||||
if (!bufs_[second].async_read_in_progress_ && DoesBufferContainData(second) &&
|
||||
IsOffsetInBuffer(offset, second)) {
|
||||
// Clear the curr_ as buffers have been swapped and curr_ contains the
|
||||
// outdated data and switch the buffers.
|
||||
if (!bufs_[curr_].async_read_in_progress_) {
|
||||
bufs_[curr_].buffer_.Clear();
|
||||
{
|
||||
// In case buffers do not align, reset second buffer. This can happen in
|
||||
// case readahead_size is set.
|
||||
if (!bufs_[second].async_read_in_progress_ &&
|
||||
!bufs_[curr_].async_read_in_progress_) {
|
||||
if (DoesBufferContainData(curr_)) {
|
||||
if (bufs_[curr_].offset_ + bufs_[curr_].buffer_.CurrentSize() !=
|
||||
bufs_[second].offset_) {
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
} else {
|
||||
if (!IsOffsetInBuffer(offset, second)) {
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If data starts from second buffer, make it curr_. Second buffer can be
|
||||
// either partial filled, full or async read is in progress.
|
||||
if (bufs_[second].async_read_in_progress_) {
|
||||
if (IsOffsetInBufferWithAsyncProgress(offset, second)) {
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
} else {
|
||||
if (DoesBufferContainData(second) && IsOffsetInBuffer(offset, second)) {
|
||||
assert(bufs_[curr_].async_read_in_progress_ ||
|
||||
bufs_[curr_].buffer_.CurrentSize() == 0);
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FilePrefetchBuffer::PollAndUpdateBuffersIfNeeded(uint64_t offset) {
|
||||
@@ -300,53 +318,16 @@ void FilePrefetchBuffer::PollAndUpdateBuffersIfNeeded(uint64_t offset) {
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
}
|
||||
|
||||
// If async_io is enabled in case of sequential reads, PrefetchAsyncInternal is
|
||||
// called. When buffers are switched, we clear the curr_ buffer as we assume the
|
||||
// data has been consumed because of sequential reads.
|
||||
// Data in buffers will always be sequential with curr_ following second and
|
||||
// not vice versa.
|
||||
//
|
||||
// Scenarios for prefetching asynchronously:
|
||||
// Case1: If both buffers are empty, prefetch n + readahead_size_/2 bytes
|
||||
// synchronously in curr_ and prefetch readahead_size_/2 async in second
|
||||
// buffer.
|
||||
// Case2: If second buffer has partial or full data, make it current and
|
||||
// prefetch readahead_size_/2 async in second buffer. In case of
|
||||
// partial data, prefetch remaining bytes from size n synchronously to
|
||||
// fulfill the requested bytes request.
|
||||
// Case3: If curr_ has partial data, prefetch remaining bytes from size n
|
||||
// synchronously in curr_ to fulfill the requested bytes request and
|
||||
// prefetch readahead_size_/2 bytes async in second buffer.
|
||||
// Case4: (Special case) If data is in both buffers, copy requested data from
|
||||
// curr_, send async request on curr_, wait for poll to fill second
|
||||
// buffer (if any), and copy remaining data from second buffer to third
|
||||
// buffer.
|
||||
Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
Status FilePrefetchBuffer::HandleOverlappingData(
|
||||
const IOOptions& opts, RandomAccessFileReader* reader, uint64_t offset,
|
||||
size_t length, size_t readahead_size, Env::IOPriority rate_limiter_priority,
|
||||
bool& copy_to_third_buffer) {
|
||||
if (!enable_) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("FilePrefetchBuffer::PrefetchAsyncInternal:Start");
|
||||
|
||||
size_t alignment = reader->file()->GetRequiredBufferAlignment();
|
||||
size_t length, size_t readahead_size,
|
||||
Env::IOPriority /*rate_limiter_priority*/, bool& copy_to_third_buffer,
|
||||
uint64_t& tmp_offset, size_t& tmp_length) {
|
||||
Status s;
|
||||
uint64_t tmp_offset = offset;
|
||||
size_t tmp_length = length;
|
||||
|
||||
// 1. Abort IO and swap buffers if needed to point curr_ to first buffer with
|
||||
// data.
|
||||
{
|
||||
if (!explicit_prefetch_submitted_) {
|
||||
AbortIOIfNeeded(offset);
|
||||
}
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
}
|
||||
size_t alignment = reader->file()->GetRequiredBufferAlignment();
|
||||
uint32_t second = curr_ ^ 1;
|
||||
|
||||
// 2. If data is overlapping over two buffers, copy the data from curr_ and
|
||||
// If data is overlapping over two buffers, copy the data from curr_ and
|
||||
// call ReadAsync on curr_.
|
||||
if (!bufs_[curr_].async_read_in_progress_ && DoesBufferContainData(curr_) &&
|
||||
IsOffsetInBuffer(offset, curr_) &&
|
||||
@@ -391,21 +372,80 @@ Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
}
|
||||
curr_ = curr_ ^ 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// If async_io is enabled in case of sequential reads, PrefetchAsyncInternal is
|
||||
// called. When buffers are switched, we clear the curr_ buffer as we assume the
|
||||
// data has been consumed because of sequential reads.
|
||||
// Data in buffers will always be sequential with curr_ following second and
|
||||
// not vice versa.
|
||||
//
|
||||
// Scenarios for prefetching asynchronously:
|
||||
// Case1: If both buffers are empty, prefetch n + readahead_size_/2 bytes
|
||||
// synchronously in curr_ and prefetch readahead_size_/2 async in second
|
||||
// buffer.
|
||||
// Case2: If second buffer has partial or full data, make it current and
|
||||
// prefetch readahead_size_/2 async in second buffer. In case of
|
||||
// partial data, prefetch remaining bytes from size n synchronously to
|
||||
// fulfill the requested bytes request.
|
||||
// Case3: If curr_ has partial data, prefetch remaining bytes from size n
|
||||
// synchronously in curr_ to fulfill the requested bytes request and
|
||||
// prefetch readahead_size_/2 bytes async in second buffer.
|
||||
// Case4: (Special case) If data is in both buffers, copy requested data from
|
||||
// curr_, send async request on curr_, wait for poll to fill second
|
||||
// buffer (if any), and copy remaining data from second buffer to third
|
||||
// buffer.
|
||||
Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
const IOOptions& opts, RandomAccessFileReader* reader, uint64_t offset,
|
||||
size_t length, size_t readahead_size, Env::IOPriority rate_limiter_priority,
|
||||
bool& copy_to_third_buffer) {
|
||||
if (!enable_) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("FilePrefetchBuffer::PrefetchAsyncInternal:Start");
|
||||
|
||||
size_t alignment = reader->file()->GetRequiredBufferAlignment();
|
||||
Status s;
|
||||
uint64_t tmp_offset = offset;
|
||||
size_t tmp_length = length;
|
||||
|
||||
// 1. Abort IO and swap buffers if needed to point curr_ to first buffer with
|
||||
// data.
|
||||
if (!explicit_prefetch_submitted_) {
|
||||
AbortIOIfNeeded(offset);
|
||||
}
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
|
||||
// 2. Handle overlapping data over two buffers. If data is overlapping then
|
||||
// during this call:
|
||||
// - data from curr_ is copied into third buffer,
|
||||
// - curr_ is send for async prefetching of further data if second buffer
|
||||
// contains remaining requested data or in progress for async prefetch,
|
||||
// - switch buffers and curr_ now points to second buffer to copy remaining
|
||||
// data.
|
||||
s = HandleOverlappingData(opts, reader, offset, length, readahead_size,
|
||||
rate_limiter_priority, copy_to_third_buffer,
|
||||
tmp_offset, tmp_length);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// 3. Call Poll only if data is needed for the second buffer.
|
||||
// - Return if whole data is in curr_ and second buffer in progress.
|
||||
// - Return if whole data is in curr_ and second buffer is in progress or
|
||||
// already full.
|
||||
// - If second buffer is empty, it will go for ReadAsync for second buffer.
|
||||
if (!bufs_[curr_].async_read_in_progress_ && DoesBufferContainData(curr_) &&
|
||||
IsDataBlockInBuffer(offset, length, curr_)) {
|
||||
// Whole data is in curr_.
|
||||
UpdateBuffersIfNeeded(offset);
|
||||
second = curr_ ^ 1;
|
||||
if (bufs_[second].async_read_in_progress_) {
|
||||
if (!IsSecondBuffEligibleForPrefetching()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
// After poll request, curr_ might be empty because of IOError in
|
||||
// callback while reading or may contain required data.
|
||||
PollAndUpdateBuffersIfNeeded(offset);
|
||||
second = curr_ ^ 1;
|
||||
}
|
||||
|
||||
if (copy_to_third_buffer) {
|
||||
@@ -427,19 +467,42 @@ Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
if (explicit_prefetch_submitted_) {
|
||||
return s;
|
||||
}
|
||||
if (!IsSecondBuffEligibleForPrefetching()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t second = curr_ ^ 1;
|
||||
assert(!bufs_[curr_].async_read_in_progress_);
|
||||
|
||||
// In case because of some IOError curr_ got empty, abort IO for second as
|
||||
// well. Otherwise data might not align if more data needs to be read in curr_
|
||||
// which might overlap with second buffer.
|
||||
if (!DoesBufferContainData(curr_) && bufs_[second].async_read_in_progress_) {
|
||||
if (bufs_[second].io_handle_ != nullptr) {
|
||||
std::vector<void*> handles;
|
||||
handles.emplace_back(bufs_[second].io_handle_);
|
||||
{
|
||||
StopWatch sw(clock_, stats_, ASYNC_PREFETCH_ABORT_MICROS);
|
||||
Status status = fs_->AbortIO(handles);
|
||||
assert(status.ok());
|
||||
}
|
||||
}
|
||||
DestroyAndClearIOHandle(second);
|
||||
bufs_[second].buffer_.Clear();
|
||||
}
|
||||
|
||||
// 5. Data is overlapping i.e. some of the data has been copied to third
|
||||
// buffer
|
||||
// and remaining will be updated below.
|
||||
if (copy_to_third_buffer) {
|
||||
// buffer and remaining will be updated below.
|
||||
if (copy_to_third_buffer && DoesBufferContainData(curr_)) {
|
||||
CopyDataToBuffer(curr_, offset, length);
|
||||
|
||||
// Length == 0: All the requested data has been copied to third buffer and
|
||||
// it has already gone for async prefetching. It can return without doing
|
||||
// anything further.
|
||||
// Length > 0: More data needs to be consumed so it will continue async and
|
||||
// sync prefetching and copy the remaining data to third buffer in the end.
|
||||
// Length > 0: More data needs to be consumed so it will continue async
|
||||
// and sync prefetching and copy the remaining data to third buffer in the
|
||||
// end.
|
||||
if (length == 0) {
|
||||
return s;
|
||||
}
|
||||
@@ -458,6 +521,9 @@ Status FilePrefetchBuffer::PrefetchAsyncInternal(
|
||||
uint64_t chunk_len1 = 0;
|
||||
uint64_t read_len1 = 0;
|
||||
|
||||
assert(!bufs_[second].async_read_in_progress_ &&
|
||||
!DoesBufferContainData(second));
|
||||
|
||||
// For length == 0, skip the synchronous prefetching. read_len1 will be 0.
|
||||
if (length > 0) {
|
||||
CalculateOffsetAndLen(alignment, offset, roundup_len1, curr_,
|
||||
@@ -594,8 +660,11 @@ bool FilePrefetchBuffer::TryReadFromCacheAsync(
|
||||
}
|
||||
|
||||
if (explicit_prefetch_submitted_) {
|
||||
if (prev_offset_ != offset) {
|
||||
// explicit_prefetch_submitted_ is special case where it expects request
|
||||
// submitted in PrefetchAsync should match with this request. Otherwise
|
||||
// buffers will be outdated.
|
||||
// Random offset called. So abort the IOs.
|
||||
if (prev_offset_ != offset) {
|
||||
AbortAllIOs();
|
||||
bufs_[curr_].buffer_.Clear();
|
||||
bufs_[curr_ ^ 1].buffer_.Clear();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <sstream>
|
||||
@@ -236,6 +237,7 @@ class FilePrefetchBuffer {
|
||||
}
|
||||
prev_offset_ = offset;
|
||||
prev_len_ = len;
|
||||
explicit_prefetch_submitted_ = false;
|
||||
}
|
||||
|
||||
void GetReadaheadState(ReadaheadFileInfo::ReadaheadInfo* readahead_info) {
|
||||
@@ -363,6 +365,27 @@ class FilePrefetchBuffer {
|
||||
bufs_[index].io_handle_ != nullptr &&
|
||||
offset >= bufs_[index].offset_ + bufs_[index].async_req_len_);
|
||||
}
|
||||
bool IsOffsetInBufferWithAsyncProgress(uint64_t offset, uint32_t index) {
|
||||
return (bufs_[index].async_read_in_progress_ &&
|
||||
offset >= bufs_[index].offset_ &&
|
||||
offset < bufs_[index].offset_ + bufs_[index].async_req_len_);
|
||||
}
|
||||
|
||||
bool IsSecondBuffEligibleForPrefetching() {
|
||||
uint32_t second = curr_ ^ 1;
|
||||
if (bufs_[second].async_read_in_progress_) {
|
||||
return false;
|
||||
}
|
||||
assert(!bufs_[curr_].async_read_in_progress_);
|
||||
|
||||
if (DoesBufferContainData(curr_) && DoesBufferContainData(second) &&
|
||||
(bufs_[curr_].offset_ + bufs_[curr_].buffer_.CurrentSize() ==
|
||||
bufs_[second].offset_)) {
|
||||
return false;
|
||||
}
|
||||
bufs_[second].buffer_.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DestroyAndClearIOHandle(uint32_t index) {
|
||||
if (bufs_[index].io_handle_ != nullptr && bufs_[index].del_fn_ != nullptr) {
|
||||
@@ -373,6 +396,13 @@ class FilePrefetchBuffer {
|
||||
bufs_[index].async_read_in_progress_ = false;
|
||||
}
|
||||
|
||||
Status HandleOverlappingData(const IOOptions& opts,
|
||||
RandomAccessFileReader* reader, uint64_t offset,
|
||||
size_t length, size_t readahead_size,
|
||||
Env::IOPriority rate_limiter_priority,
|
||||
bool& copy_to_third_buffer, uint64_t& tmp_offset,
|
||||
size_t& tmp_length);
|
||||
|
||||
std::vector<BufferInfo> bufs_;
|
||||
// curr_ represents the index for bufs_ indicating which buffer is being
|
||||
// consumed currently.
|
||||
|
||||
@@ -471,6 +471,7 @@ IOStatus RandomAccessFileReader::ReadAsync(
|
||||
(uintptr_t(req.scratch) & (alignment - 1)) == 0;
|
||||
read_async_info->is_aligned_ = is_aligned;
|
||||
|
||||
uint64_t elapsed = 0;
|
||||
if (use_direct_io() && is_aligned == false) {
|
||||
FSReadRequest aligned_req = Align(req, alignment);
|
||||
aligned_req.status.PermitUncheckedError();
|
||||
@@ -491,12 +492,17 @@ IOStatus RandomAccessFileReader::ReadAsync(
|
||||
|
||||
assert(read_async_info->buf_.CurrentSize() == 0);
|
||||
|
||||
StopWatch sw(clock_, nullptr /*stats*/, 0 /*hist_type*/, &elapsed,
|
||||
true /*overwrite*/, true /*delay_enabled*/);
|
||||
s = file_->ReadAsync(aligned_req, opts, read_async_callback,
|
||||
read_async_info, io_handle, del_fn, nullptr /*dbg*/);
|
||||
} else {
|
||||
StopWatch sw(clock_, nullptr /*stats*/, 0 /*hist_type*/, &elapsed,
|
||||
true /*overwrite*/, true /*delay_enabled*/);
|
||||
s = file_->ReadAsync(req, opts, read_async_callback, read_async_info,
|
||||
io_handle, del_fn, nullptr /*dbg*/);
|
||||
}
|
||||
RecordTick(stats_, READ_ASYNC_MICROS, elapsed);
|
||||
|
||||
// Suppress false positive clang analyzer warnings.
|
||||
// Memory is not released if file_->ReadAsync returns !s.ok(), because
|
||||
@@ -575,6 +581,8 @@ void RandomAccessFileReader::ReadAsyncCallback(const FSReadRequest& req,
|
||||
}
|
||||
if (req.status.ok()) {
|
||||
RecordInHistogram(stats_, ASYNC_READ_BYTES, req.result.size());
|
||||
} else if (!req.status.IsAborted()) {
|
||||
RecordTick(stats_, ASYNC_READ_ERROR_COUNT, 1);
|
||||
}
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (ShouldNotifyListeners()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2022, Meta, Inc. All rights reserved.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// Copyright (c) 2022, Meta Platforms, Inc. and affiliates. All rights
|
||||
// reserved. This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
|
||||
+25
-18
@@ -1285,30 +1285,37 @@ struct DBOptions {
|
||||
// Default: nullptr
|
||||
std::shared_ptr<FileChecksumGenFactory> file_checksum_gen_factory = nullptr;
|
||||
|
||||
// By default, RocksDB recovery fails if any table/blob file referenced in
|
||||
// By default, RocksDB recovery fails if any table/blob file referenced in the
|
||||
// final version reconstructed from the
|
||||
// MANIFEST are missing after scanning the MANIFEST pointed to by the
|
||||
// CURRENT file.
|
||||
// Best-efforts recovery is another recovery mode that tolerates missing or
|
||||
// corrupted table or blob files.
|
||||
// CURRENT file. It can also fail if verification of unique SST id fails.
|
||||
// Best-efforts recovery is another recovery mode that does not necessarily
|
||||
// fail when certain table/blob files are missing/corrupted or have mismatched
|
||||
// unique id table property. Instead, best-efforts recovery recovers each
|
||||
// column family to a point in the MANIFEST that corresponds to a version. In
|
||||
// such a version, all valid table/blob files referenced have the expected
|
||||
// file size. For table files, their unique id table property match the
|
||||
// MANIFEST.
|
||||
//
|
||||
// Best-efforts recovery does not need a valid CURRENT file, and tries to
|
||||
// recover the database using one of the available MANIFEST files in the db
|
||||
// directory.
|
||||
// Best-efforts recovery recovers database to a state in which the database
|
||||
// includes only table and blob files whose actual sizes match the
|
||||
// information in the chosen MANIFEST without holes in the history.
|
||||
// Best-efforts recovery tries the available MANIFEST files from high file
|
||||
// numbers (newer) to low file numbers (older), and stops after finding the
|
||||
// first MANIFEST file from which the db can be recovered to a state without
|
||||
// invalid (missing/file-mismatch) table and blob files.
|
||||
// It is possible that the database can be restored to an empty state with no
|
||||
// table or blob files.
|
||||
// Regardless of this option, the IDENTITY file is updated if needed during
|
||||
// recovery to match the DB ID in the MANIFEST (if previously using
|
||||
// write_dbid_to_manifest) or to be in some valid state (non-empty DB ID).
|
||||
// Currently, not compatible with atomic flush. Furthermore, WAL files will
|
||||
// not be used for recovery if best_efforts_recovery is true.
|
||||
// Also requires either 1) LOCK file exists or 2) underlying env's LockFile()
|
||||
// call returns ok even for non-existing LOCK file.
|
||||
// invalid (missing/filesize-mismatch/unique-id-mismatch) table and blob
|
||||
// files. It is possible that the database can be restored to an empty state
|
||||
// with no table or blob files.
|
||||
//
|
||||
// Regardless of this option, the IDENTITY file
|
||||
// is updated if needed during recovery to match the DB ID in the MANIFEST (if
|
||||
// previously using write_dbid_to_manifest) or to be in some valid state
|
||||
// (non-empty DB ID). Currently, not compatible with atomic flush.
|
||||
// Furthermore, WAL files will not be used for recovery if
|
||||
// best_efforts_recovery is true. Also requires either 1) LOCK file exists or
|
||||
// 2) underlying env's LockFile() call returns ok even for non-existing LOCK
|
||||
// file.
|
||||
//
|
||||
// Default: false
|
||||
bool best_efforts_recovery = false;
|
||||
|
||||
@@ -1480,7 +1487,7 @@ struct ReadOptions {
|
||||
const Slice* iterate_lower_bound;
|
||||
|
||||
// "iterate_upper_bound" defines the extent up to which the forward iterator
|
||||
// can returns entries. Once the bound is reached, Valid() will be false.
|
||||
// can return entries. Once the bound is reached, Valid() will be false.
|
||||
// "iterate_upper_bound" is exclusive ie the bound value is
|
||||
// not a valid entry. If prefix_extractor is not null:
|
||||
// 1. If options.auto_prefix_mode = true, iterate_upper_bound will be used
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "rocksdb/iterator.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/slice.h"
|
||||
@@ -31,9 +30,6 @@ class SstFileReader {
|
||||
// If "snapshot" is nullptr, the iterator returns only the latest keys.
|
||||
Iterator* NewIterator(const ReadOptions& options);
|
||||
|
||||
FragmentedRangeTombstoneIterator* NewRangeTombstoneIterator(
|
||||
const ReadOptions& options);
|
||||
|
||||
std::shared_ptr<const TableProperties> GetTableProperties() const;
|
||||
|
||||
// Verifies whether there is corruption in this table.
|
||||
|
||||
@@ -448,6 +448,11 @@ enum Tickers : uint32_t {
|
||||
// # of bytes written into blob cache.
|
||||
BLOB_DB_CACHE_BYTES_WRITE,
|
||||
|
||||
// Time spent in the ReadAsync file system call
|
||||
READ_ASYNC_MICROS,
|
||||
// Number of errors returned to the async read callback
|
||||
ASYNC_READ_ERROR_COUNT,
|
||||
|
||||
TICKER_ENUM_MAX
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ struct ConfigOptions;
|
||||
// operating system time-related functionality.
|
||||
class SystemClock : public Customizable {
|
||||
public:
|
||||
virtual ~SystemClock() {}
|
||||
~SystemClock() override {}
|
||||
|
||||
static const char* Type() { return "SystemClock"; }
|
||||
static Status CreateFromString(const ConfigOptions& options,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 7
|
||||
#define ROCKSDB_MINOR 8
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_PATCH 3
|
||||
|
||||
// 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
|
||||
|
||||
@@ -5104,6 +5104,10 @@ class TickerTypeJni {
|
||||
return -0x33;
|
||||
case ROCKSDB_NAMESPACE::Tickers::BLOB_DB_CACHE_BYTES_WRITE:
|
||||
return -0x34;
|
||||
case ROCKSDB_NAMESPACE::Tickers::READ_ASYNC_MICROS:
|
||||
return -0x35;
|
||||
case ROCKSDB_NAMESPACE::Tickers::ASYNC_READ_ERROR_COUNT:
|
||||
return -0x36;
|
||||
case ROCKSDB_NAMESPACE::Tickers::TICKER_ENUM_MAX:
|
||||
// 0x5F was the max value in the initial copy of tickers to Java.
|
||||
// Since these values are exposed directly to Java clients, we keep
|
||||
@@ -5487,6 +5491,10 @@ class TickerTypeJni {
|
||||
return ROCKSDB_NAMESPACE::Tickers::BLOB_DB_CACHE_BYTES_READ;
|
||||
case -0x34:
|
||||
return ROCKSDB_NAMESPACE::Tickers::BLOB_DB_CACHE_BYTES_WRITE;
|
||||
case -0x35:
|
||||
return ROCKSDB_NAMESPACE::Tickers::READ_ASYNC_MICROS;
|
||||
case -0x36:
|
||||
return ROCKSDB_NAMESPACE::Tickers::ASYNC_READ_ERROR_COUNT;
|
||||
case 0x5F:
|
||||
// 0x5F was the max value in the initial copy of tickers to Java.
|
||||
// Since these values are exposed directly to Java clients, we keep
|
||||
|
||||
+81
-16
@@ -8,7 +8,9 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include "memory/arena.h"
|
||||
|
||||
#ifndef OS_WIN
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
|
||||
#include "logging/logging.h"
|
||||
@@ -20,7 +22,16 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
size_t Arena::OptimizeBlockSize(size_t block_size) {
|
||||
// MSVC complains that it is already defined since it is static in the header.
|
||||
#ifndef _MSC_VER
|
||||
const size_t Arena::kInlineSize;
|
||||
#endif
|
||||
|
||||
const size_t Arena::kMinBlockSize = 4096;
|
||||
const size_t Arena::kMaxBlockSize = 2u << 30;
|
||||
static const int kAlignUnit = alignof(max_align_t);
|
||||
|
||||
size_t OptimizeBlockSize(size_t block_size) {
|
||||
// Make sure block_size is in optimal range
|
||||
block_size = std::max(Arena::kMinBlockSize, block_size);
|
||||
block_size = std::min(Arena::kMaxBlockSize, block_size);
|
||||
@@ -42,12 +53,14 @@ Arena::Arena(size_t block_size, AllocTracker* tracker, size_t huge_page_size)
|
||||
blocks_memory_ += alloc_bytes_remaining_;
|
||||
aligned_alloc_ptr_ = inline_block_;
|
||||
unaligned_alloc_ptr_ = inline_block_ + alloc_bytes_remaining_;
|
||||
if (MemMapping::kHugePageSupported) {
|
||||
#ifdef MAP_HUGETLB
|
||||
hugetlb_size_ = huge_page_size;
|
||||
if (hugetlb_size_ && kBlockSize > hugetlb_size_) {
|
||||
hugetlb_size_ = ((kBlockSize - 1U) / hugetlb_size_ + 1U) * hugetlb_size_;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)huge_page_size;
|
||||
#endif
|
||||
if (tracker_ != nullptr) {
|
||||
tracker_->Allocate(kInlineSize);
|
||||
}
|
||||
@@ -58,6 +71,21 @@ Arena::~Arena() {
|
||||
assert(tracker_->is_freed());
|
||||
tracker_->FreeMem();
|
||||
}
|
||||
for (const auto& block : blocks_) {
|
||||
delete[] block;
|
||||
}
|
||||
|
||||
#ifdef MAP_HUGETLB
|
||||
for (const auto& mmap_info : huge_blocks_) {
|
||||
if (mmap_info.addr_ == nullptr) {
|
||||
continue;
|
||||
}
|
||||
auto ret = munmap(mmap_info.addr_, mmap_info.length_);
|
||||
if (ret != 0) {
|
||||
// TODO(sdong): Better handling
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
char* Arena::AllocateFallback(size_t bytes, bool aligned) {
|
||||
@@ -71,10 +99,12 @@ char* Arena::AllocateFallback(size_t bytes, bool aligned) {
|
||||
// We waste the remaining space in the current block.
|
||||
size_t size = 0;
|
||||
char* block_head = nullptr;
|
||||
if (MemMapping::kHugePageSupported && hugetlb_size_ > 0) {
|
||||
#ifdef MAP_HUGETLB
|
||||
if (hugetlb_size_) {
|
||||
size = hugetlb_size_;
|
||||
block_head = AllocateFromHugePage(size);
|
||||
}
|
||||
#endif
|
||||
if (!block_head) {
|
||||
size = kBlockSize;
|
||||
block_head = AllocateNewBlock(size);
|
||||
@@ -93,22 +123,45 @@ char* Arena::AllocateFallback(size_t bytes, bool aligned) {
|
||||
}
|
||||
|
||||
char* Arena::AllocateFromHugePage(size_t bytes) {
|
||||
MemMapping mm = MemMapping::AllocateHuge(bytes);
|
||||
auto addr = static_cast<char*>(mm.Get());
|
||||
if (addr) {
|
||||
huge_blocks_.push_back(std::move(mm));
|
||||
#ifdef MAP_HUGETLB
|
||||
if (hugetlb_size_ == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// Reserve space in `huge_blocks_` before calling `mmap`.
|
||||
// Use `emplace_back()` instead of `reserve()` to let std::vector manage its
|
||||
// own memory and do fewer reallocations.
|
||||
//
|
||||
// - If `emplace_back` throws, no memory leaks because we haven't called
|
||||
// `mmap` yet.
|
||||
// - If `mmap` throws, no memory leaks because the vector will be cleaned up
|
||||
// via RAII.
|
||||
huge_blocks_.emplace_back(nullptr /* addr */, 0 /* length */);
|
||||
|
||||
void* addr = mmap(nullptr, bytes, (PROT_READ | PROT_WRITE),
|
||||
(MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB), -1, 0);
|
||||
|
||||
if (addr == MAP_FAILED) {
|
||||
return nullptr;
|
||||
}
|
||||
huge_blocks_.back() = MmapInfo(addr, bytes);
|
||||
blocks_memory_ += bytes;
|
||||
if (tracker_ != nullptr) {
|
||||
tracker_->Allocate(bytes);
|
||||
}
|
||||
}
|
||||
return addr;
|
||||
return reinterpret_cast<char*>(addr);
|
||||
#else
|
||||
(void)bytes;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
char* Arena::AllocateAligned(size_t bytes, size_t huge_page_size,
|
||||
Logger* logger) {
|
||||
if (MemMapping::kHugePageSupported && hugetlb_size_ > 0 &&
|
||||
huge_page_size > 0 && bytes > 0) {
|
||||
assert((kAlignUnit & (kAlignUnit - 1)) ==
|
||||
0); // Pointer size should be a power of 2
|
||||
|
||||
#ifdef MAP_HUGETLB
|
||||
if (huge_page_size > 0 && bytes > 0) {
|
||||
// Allocate from a huge page TLB table.
|
||||
size_t reserved_size =
|
||||
((bytes - 1U) / huge_page_size + 1U) * huge_page_size;
|
||||
@@ -124,6 +177,10 @@ char* Arena::AllocateAligned(size_t bytes, size_t huge_page_size,
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)huge_page_size;
|
||||
(void)logger;
|
||||
#endif
|
||||
|
||||
size_t current_mod =
|
||||
reinterpret_cast<uintptr_t>(aligned_alloc_ptr_) & (kAlignUnit - 1);
|
||||
@@ -143,10 +200,17 @@ char* Arena::AllocateAligned(size_t bytes, size_t huge_page_size,
|
||||
}
|
||||
|
||||
char* Arena::AllocateNewBlock(size_t block_bytes) {
|
||||
auto uniq = std::make_unique<char[]>(block_bytes);
|
||||
char* block = uniq.get();
|
||||
blocks_.push_back(std::move(uniq));
|
||||
// Reserve space in `blocks_` before allocating memory via new.
|
||||
// Use `emplace_back()` instead of `reserve()` to let std::vector manage its
|
||||
// own memory and do fewer reallocations.
|
||||
//
|
||||
// - If `emplace_back` throws, no memory leaks because we haven't called `new`
|
||||
// yet.
|
||||
// - If `new` throws, no memory leaks because the vector will be cleaned up
|
||||
// via RAII.
|
||||
blocks_.emplace_back(nullptr);
|
||||
|
||||
char* block = new char[block_bytes];
|
||||
size_t allocated_size;
|
||||
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
allocated_size = malloc_usable_size(block);
|
||||
@@ -163,6 +227,7 @@ char* Arena::AllocateNewBlock(size_t block_bytes) {
|
||||
if (tracker_ != nullptr) {
|
||||
tracker_->Allocate(allocated_size);
|
||||
}
|
||||
blocks_.back() = block;
|
||||
return block;
|
||||
}
|
||||
|
||||
|
||||
+31
-25
@@ -12,13 +12,16 @@
|
||||
// size, it uses malloc to directly get the requested size.
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef OS_WIN
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
|
||||
#include <vector>
|
||||
#include "memory/allocator.h"
|
||||
#include "port/mmap.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "util/mutexlock.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -28,13 +31,9 @@ class Arena : public Allocator {
|
||||
Arena(const Arena&) = delete;
|
||||
void operator=(const Arena&) = delete;
|
||||
|
||||
static constexpr size_t kInlineSize = 2048;
|
||||
static constexpr size_t kMinBlockSize = 4096;
|
||||
static constexpr size_t kMaxBlockSize = 2u << 30;
|
||||
|
||||
static constexpr unsigned kAlignUnit = alignof(std::max_align_t);
|
||||
static_assert((kAlignUnit & (kAlignUnit - 1)) == 0,
|
||||
"Pointer size should be power of 2");
|
||||
static const size_t kInlineSize = 2048;
|
||||
static const size_t kMinBlockSize;
|
||||
static const size_t kMaxBlockSize;
|
||||
|
||||
// huge_page_size: if 0, don't use huge page TLB. If > 0 (should set to the
|
||||
// supported hugepage size of the system), block allocation will try huge
|
||||
@@ -64,7 +63,7 @@ class Arena : public Allocator {
|
||||
// by the arena (exclude the space allocated but not yet used for future
|
||||
// allocations).
|
||||
size_t ApproximateMemoryUsage() const {
|
||||
return blocks_memory_ + blocks_.size() * sizeof(char*) -
|
||||
return blocks_memory_ + blocks_.capacity() * sizeof(char*) -
|
||||
alloc_bytes_remaining_;
|
||||
}
|
||||
|
||||
@@ -82,19 +81,21 @@ class Arena : public Allocator {
|
||||
return blocks_.empty() && huge_blocks_.empty();
|
||||
}
|
||||
|
||||
// check and adjust the block_size so that the return value is
|
||||
// 1. in the range of [kMinBlockSize, kMaxBlockSize].
|
||||
// 2. the multiple of align unit.
|
||||
static size_t OptimizeBlockSize(size_t block_size);
|
||||
|
||||
private:
|
||||
alignas(std::max_align_t) char inline_block_[kInlineSize];
|
||||
char inline_block_[kInlineSize] __attribute__((__aligned__(alignof(max_align_t))));
|
||||
// Number of bytes allocated in one block
|
||||
const size_t kBlockSize;
|
||||
// Allocated memory blocks
|
||||
std::deque<std::unique_ptr<char[]>> blocks_;
|
||||
// Huge page allocations
|
||||
std::deque<MemMapping> huge_blocks_;
|
||||
// Array of new[] allocated memory blocks
|
||||
using Blocks = std::vector<char*>;
|
||||
Blocks blocks_;
|
||||
|
||||
struct MmapInfo {
|
||||
void* addr_;
|
||||
size_t length_;
|
||||
|
||||
MmapInfo(void* addr, size_t length) : addr_(addr), length_(length) {}
|
||||
};
|
||||
std::vector<MmapInfo> huge_blocks_;
|
||||
size_t irregular_block_num = 0;
|
||||
|
||||
// Stats for current active block.
|
||||
@@ -107,15 +108,15 @@ class Arena : public Allocator {
|
||||
// How many bytes left in currently active block?
|
||||
size_t alloc_bytes_remaining_ = 0;
|
||||
|
||||
#ifdef MAP_HUGETLB
|
||||
size_t hugetlb_size_ = 0;
|
||||
|
||||
#endif // MAP_HUGETLB
|
||||
char* AllocateFromHugePage(size_t bytes);
|
||||
char* AllocateFallback(size_t bytes, bool aligned);
|
||||
char* AllocateNewBlock(size_t block_bytes);
|
||||
|
||||
// Bytes of memory in blocks allocated so far
|
||||
size_t blocks_memory_ = 0;
|
||||
// Non-owned
|
||||
AllocTracker* tracker_;
|
||||
};
|
||||
|
||||
@@ -132,4 +133,9 @@ inline char* Arena::Allocate(size_t bytes) {
|
||||
return AllocateFallback(bytes, false /* unaligned */);
|
||||
}
|
||||
|
||||
// check and adjust the block_size so that the return value is
|
||||
// 1. in the range of [kMinBlockSize, kMaxBlockSize].
|
||||
// 2. the multiple of align unit.
|
||||
extern size_t OptimizeBlockSize(size_t block_size);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include "memory/arena.h"
|
||||
|
||||
#ifndef OS_WIN
|
||||
#include <sys/resource.h>
|
||||
#endif
|
||||
#include "port/port.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/random.h"
|
||||
|
||||
@@ -201,61 +196,6 @@ TEST_F(ArenaTest, Simple) {
|
||||
SimpleTest(0);
|
||||
SimpleTest(kHugePageSize);
|
||||
}
|
||||
|
||||
// Number of minor page faults since last call
|
||||
size_t PopMinorPageFaultCount() {
|
||||
#ifdef RUSAGE_SELF
|
||||
static long prev = 0;
|
||||
struct rusage usage;
|
||||
EXPECT_EQ(getrusage(RUSAGE_SELF, &usage), 0);
|
||||
size_t rv = usage.ru_minflt - prev;
|
||||
prev = usage.ru_minflt;
|
||||
return rv;
|
||||
#else
|
||||
// Conservative
|
||||
return SIZE_MAX;
|
||||
#endif // RUSAGE_SELF
|
||||
}
|
||||
|
||||
TEST(MmapTest, AllocateLazyZeroed) {
|
||||
// Doesn't have to be page aligned
|
||||
constexpr size_t len = 1234567;
|
||||
MemMapping m = MemMapping::AllocateLazyZeroed(len);
|
||||
auto arr = static_cast<char*>(m.Get());
|
||||
|
||||
// Should generally work
|
||||
ASSERT_NE(arr, nullptr);
|
||||
|
||||
// Start counting page faults
|
||||
PopMinorPageFaultCount();
|
||||
|
||||
// Access half of the allocation
|
||||
size_t i = 0;
|
||||
for (; i < len / 2; ++i) {
|
||||
ASSERT_EQ(arr[i], 0);
|
||||
arr[i] = static_cast<char>(i & 255);
|
||||
}
|
||||
|
||||
// Appropriate page faults (maybe more)
|
||||
size_t faults = PopMinorPageFaultCount();
|
||||
ASSERT_GE(faults, len / 2 / port::kPageSize);
|
||||
|
||||
// Access rest of the allocation
|
||||
for (; i < len; ++i) {
|
||||
ASSERT_EQ(arr[i], 0);
|
||||
arr[i] = static_cast<char>(i & 255);
|
||||
}
|
||||
|
||||
// Appropriate page faults (maybe more)
|
||||
faults = PopMinorPageFaultCount();
|
||||
ASSERT_GE(faults, len / 2 / port::kPageSize);
|
||||
|
||||
// Verify data
|
||||
for (i = 0; i < len; ++i) {
|
||||
ASSERT_EQ(arr[i], static_cast<char>(i & 255));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -233,7 +233,9 @@ const std::vector<std::pair<Tickers, std::string>> TickersNameMap = {
|
||||
{BLOB_DB_CACHE_ADD, "rocksdb.blobdb.cache.add"},
|
||||
{BLOB_DB_CACHE_ADD_FAILURES, "rocksdb.blobdb.cache.add.failures"},
|
||||
{BLOB_DB_CACHE_BYTES_READ, "rocksdb.blobdb.cache.bytes.read"},
|
||||
{BLOB_DB_CACHE_BYTES_WRITE, "rocksdb.blobdb.cache.bytes.write"}};
|
||||
{BLOB_DB_CACHE_BYTES_WRITE, "rocksdb.blobdb.cache.bytes.write"},
|
||||
{READ_ASYNC_MICROS, "rocksdb.read.async.micros"},
|
||||
{ASYNC_READ_ERROR_COUNT, "rocksdb.async.read.error.count"}};
|
||||
|
||||
const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
|
||||
{DB_GET, "rocksdb.db.get.micros"},
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "port/mmap.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
#include "util/hash.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
MemMapping::~MemMapping() {
|
||||
#ifdef OS_WIN
|
||||
if (addr_ != nullptr) {
|
||||
(void)::UnmapViewOfFile(addr_);
|
||||
}
|
||||
if (page_file_handle_ != NULL) {
|
||||
(void)::CloseHandle(page_file_handle_);
|
||||
}
|
||||
#else // OS_WIN -> !OS_WIN
|
||||
if (addr_ != nullptr) {
|
||||
auto status = munmap(addr_, length_);
|
||||
assert(status == 0);
|
||||
if (status != 0) {
|
||||
// TODO: handle error?
|
||||
}
|
||||
}
|
||||
#endif // OS_WIN
|
||||
}
|
||||
|
||||
MemMapping::MemMapping(MemMapping&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
MemMapping& MemMapping::operator=(MemMapping&& other) noexcept {
|
||||
if (&other == this) {
|
||||
return *this;
|
||||
}
|
||||
this->~MemMapping();
|
||||
std::memcpy(this, &other, sizeof(*this));
|
||||
new (&other) MemMapping();
|
||||
return *this;
|
||||
}
|
||||
|
||||
MemMapping MemMapping::AllocateAnonymous(size_t length, bool huge) {
|
||||
MemMapping mm;
|
||||
mm.length_ = length;
|
||||
assert(mm.addr_ == nullptr);
|
||||
if (length == 0) {
|
||||
// OK to leave addr as nullptr
|
||||
return mm;
|
||||
}
|
||||
int huge_flag = 0;
|
||||
#ifdef OS_WIN
|
||||
if (huge) {
|
||||
#ifdef FILE_MAP_LARGE_PAGES
|
||||
huge_flag = FILE_MAP_LARGE_PAGES;
|
||||
#endif // FILE_MAP_LARGE_PAGES
|
||||
}
|
||||
mm.page_file_handle_ = ::CreateFileMapping(
|
||||
INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE | SEC_COMMIT,
|
||||
Upper32of64(length), Lower32of64(length), nullptr);
|
||||
if (mm.page_file_handle_ == NULL) {
|
||||
// Failure
|
||||
return mm;
|
||||
}
|
||||
mm.addr_ = ::MapViewOfFile(mm.page_file_handle_, FILE_MAP_WRITE | huge_flag,
|
||||
0, 0, length);
|
||||
#else // OS_WIN -> !OS_WIN
|
||||
if (huge) {
|
||||
#ifdef MAP_HUGETLB
|
||||
huge_flag = MAP_HUGETLB;
|
||||
#endif // MAP_HUGE_TLB
|
||||
}
|
||||
mm.addr_ = mmap(nullptr, length, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | huge_flag, -1, 0);
|
||||
if (mm.addr_ == MAP_FAILED) {
|
||||
mm.addr_ = nullptr;
|
||||
}
|
||||
#endif // OS_WIN
|
||||
return mm;
|
||||
}
|
||||
|
||||
MemMapping MemMapping::AllocateHuge(size_t length) {
|
||||
return AllocateAnonymous(length, /*huge*/ true);
|
||||
}
|
||||
|
||||
MemMapping MemMapping::AllocateLazyZeroed(size_t length) {
|
||||
return AllocateAnonymous(length, /*huge*/ false);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef OS_WIN
|
||||
#include <windows.h>
|
||||
// ^^^ Must come first
|
||||
#include <memoryapi.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif // OS_WIN
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// An RAII wrapper for mmaped memory
|
||||
class MemMapping {
|
||||
public:
|
||||
static constexpr bool kHugePageSupported =
|
||||
#if defined(MAP_HUGETLB) || defined(FILE_MAP_LARGE_PAGES)
|
||||
true;
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
|
||||
// Allocate memory requesting to be backed by huge pages
|
||||
static MemMapping AllocateHuge(size_t length);
|
||||
|
||||
// Allocate memory that is only lazily mapped to resident memory and
|
||||
// guaranteed to be zero-initialized. Note that some platforms like
|
||||
// Linux allow memory over-commit, where only the used portion of memory
|
||||
// matters, while other platforms require enough swap space (page file) to
|
||||
// back the full mapping.
|
||||
static MemMapping AllocateLazyZeroed(size_t length);
|
||||
|
||||
// No copies
|
||||
MemMapping(const MemMapping&) = delete;
|
||||
MemMapping& operator=(const MemMapping&) = delete;
|
||||
// Move
|
||||
MemMapping(MemMapping&&) noexcept;
|
||||
MemMapping& operator=(MemMapping&&) noexcept;
|
||||
|
||||
// Releases the mapping
|
||||
~MemMapping();
|
||||
|
||||
inline void* Get() const { return addr_; }
|
||||
inline size_t Length() const { return length_; }
|
||||
|
||||
private:
|
||||
MemMapping() {}
|
||||
|
||||
// The mapped memory, or nullptr on failure / not supported
|
||||
void* addr_ = nullptr;
|
||||
// The known usable number of bytes starting at that address
|
||||
size_t length_ = 0;
|
||||
|
||||
#ifdef OS_WIN
|
||||
HANDLE page_file_handle_ = NULL;
|
||||
#endif // OS_WIN
|
||||
|
||||
static MemMapping AllocateAnonymous(size_t length, bool huge);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -154,7 +154,6 @@ LIB_SOURCES = \
|
||||
options/options.cc \
|
||||
options/options_helper.cc \
|
||||
options/options_parser.cc \
|
||||
port/mmap.cc \
|
||||
port/port_posix.cc \
|
||||
port/win/env_default.cc \
|
||||
port/win/env_win.cc \
|
||||
|
||||
+66
-21
@@ -117,14 +117,16 @@ class MergingIterator : public InternalIterator {
|
||||
public:
|
||||
MergingIterator(const InternalKeyComparator* comparator,
|
||||
InternalIterator** children, int n, bool is_arena_mode,
|
||||
bool prefix_seek_mode)
|
||||
bool prefix_seek_mode,
|
||||
const Slice* iterate_upper_bound = nullptr)
|
||||
: is_arena_mode_(is_arena_mode),
|
||||
prefix_seek_mode_(prefix_seek_mode),
|
||||
direction_(kForward),
|
||||
comparator_(comparator),
|
||||
current_(nullptr),
|
||||
minHeap_(comparator_),
|
||||
pinned_iters_mgr_(nullptr) {
|
||||
pinned_iters_mgr_(nullptr),
|
||||
iterate_upper_bound_(iterate_upper_bound) {
|
||||
children_.resize(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
children_[i].level = i;
|
||||
@@ -197,27 +199,48 @@ class MergingIterator : public InternalIterator {
|
||||
// Add range_tombstone_iters_[level] into min heap.
|
||||
// Updates active_ if the end key of a range tombstone is inserted.
|
||||
// @param start_key specifies which end point of the range tombstone to add.
|
||||
void InsertRangeTombstoneToMinHeap(size_t level, bool start_key = true) {
|
||||
void InsertRangeTombstoneToMinHeap(size_t level, bool start_key = true,
|
||||
bool replace_top = false) {
|
||||
assert(!range_tombstone_iters_.empty() &&
|
||||
range_tombstone_iters_[level]->Valid());
|
||||
if (start_key) {
|
||||
pinned_heap_item_[level].SetTombstoneKey(
|
||||
range_tombstone_iters_[level]->start_key());
|
||||
ParsedInternalKey pik = range_tombstone_iters_[level]->start_key();
|
||||
// iterate_upper_bound does not have timestamp
|
||||
if (iterate_upper_bound_ &&
|
||||
comparator_->user_comparator()->CompareWithoutTimestamp(
|
||||
pik.user_key, true /* a_has_ts */, *iterate_upper_bound_,
|
||||
false /* b_has_ts */) >= 0) {
|
||||
if (replace_top) {
|
||||
// replace_top implies this range tombstone iterator is still in
|
||||
// minHeap_ and at the top.
|
||||
minHeap_.pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
pinned_heap_item_[level].SetTombstoneKey(std::move(pik));
|
||||
pinned_heap_item_[level].type = HeapItem::DELETE_RANGE_START;
|
||||
assert(active_.count(level) == 0);
|
||||
} else {
|
||||
// allow end key to go over upper bound (if present) since start key is
|
||||
// before upper bound and the range tombstone could still cover a
|
||||
// range before upper bound.
|
||||
pinned_heap_item_[level].SetTombstoneKey(
|
||||
range_tombstone_iters_[level]->end_key());
|
||||
pinned_heap_item_[level].type = HeapItem::DELETE_RANGE_END;
|
||||
active_.insert(level);
|
||||
}
|
||||
if (replace_top) {
|
||||
minHeap_.replace_top(&pinned_heap_item_[level]);
|
||||
} else {
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add range_tombstone_iters_[level] into max heap.
|
||||
// Updates active_ if the start key of a range tombstone is inserted.
|
||||
// @param end_key specifies which end point of the range tombstone to add.
|
||||
void InsertRangeTombstoneToMaxHeap(size_t level, bool end_key = true) {
|
||||
void InsertRangeTombstoneToMaxHeap(size_t level, bool end_key = true,
|
||||
bool replace_top = false) {
|
||||
assert(!range_tombstone_iters_.empty() &&
|
||||
range_tombstone_iters_[level]->Valid());
|
||||
if (end_key) {
|
||||
@@ -231,8 +254,12 @@ class MergingIterator : public InternalIterator {
|
||||
pinned_heap_item_[level].type = HeapItem::DELETE_RANGE_START;
|
||||
active_.insert(level);
|
||||
}
|
||||
if (replace_top) {
|
||||
maxHeap_->replace_top(&pinned_heap_item_[level]);
|
||||
} else {
|
||||
maxHeap_->push(&pinned_heap_item_[level]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove HeapItems from top of minHeap_ that are of type DELETE_RANGE_START
|
||||
// until minHeap_ is empty or the top of the minHeap_ is not of type
|
||||
@@ -241,10 +268,10 @@ class MergingIterator : public InternalIterator {
|
||||
void PopDeleteRangeStart() {
|
||||
while (!minHeap_.empty() &&
|
||||
minHeap_.top()->type == HeapItem::DELETE_RANGE_START) {
|
||||
auto level = minHeap_.top()->level;
|
||||
minHeap_.pop();
|
||||
TEST_SYNC_POINT_CALLBACK("MergeIterator::PopDeleteRangeStart", nullptr);
|
||||
// insert end key of this range tombstone and updates active_
|
||||
InsertRangeTombstoneToMinHeap(level, false /* start_key */);
|
||||
InsertRangeTombstoneToMinHeap(
|
||||
minHeap_.top()->level, false /* start_key */, true /* replace_top */);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,10 +282,9 @@ class MergingIterator : public InternalIterator {
|
||||
void PopDeleteRangeEnd() {
|
||||
while (!maxHeap_->empty() &&
|
||||
maxHeap_->top()->type == HeapItem::DELETE_RANGE_END) {
|
||||
auto level = maxHeap_->top()->level;
|
||||
maxHeap_->pop();
|
||||
// insert start key of this range tombstone and updates active_
|
||||
InsertRangeTombstoneToMaxHeap(level, false /* end_key */);
|
||||
InsertRangeTombstoneToMaxHeap(maxHeap_->top()->level, false /* end_key */,
|
||||
true /* replace_top */);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +591,10 @@ class MergingIterator : public InternalIterator {
|
||||
std::unique_ptr<MergerMaxIterHeap> maxHeap_;
|
||||
PinnedIteratorsManager* pinned_iters_mgr_;
|
||||
|
||||
// Used to bound range tombstones. For point keys, DBIter and SSTable iterator
|
||||
// take care of boundary checking.
|
||||
const Slice* iterate_upper_bound_;
|
||||
|
||||
// In forward direction, process a child that is not in the min heap.
|
||||
// If valid, add to the min heap. Otherwise, check status.
|
||||
void AddToMinHeapOrCheckStatus(HeapItem*);
|
||||
@@ -626,9 +656,19 @@ void MergingIterator::SeekImpl(const Slice& target, size_t starting_level,
|
||||
for (size_t level = 0; level < starting_level; ++level) {
|
||||
if (range_tombstone_iters_[level] &&
|
||||
range_tombstone_iters_[level]->Valid()) {
|
||||
assert(static_cast<bool>(active_.count(level)) ==
|
||||
(pinned_heap_item_[level].type == HeapItem::DELETE_RANGE_END));
|
||||
// use an iterator on active_ if performance becomes an issue here
|
||||
if (active_.count(level) > 0) {
|
||||
assert(pinned_heap_item_[level].type == HeapItem::DELETE_RANGE_END);
|
||||
// if it was active, then start key must be within upper_bound,
|
||||
// so we can add to minHeap_ directly.
|
||||
minHeap_.push(&pinned_heap_item_[level]);
|
||||
} else {
|
||||
// this takes care of checking iterate_upper_bound, but with an extra
|
||||
// key comparison if range_tombstone_iters_[level] was already out of
|
||||
// bound. Consider using a new HeapItem type or some flag to remember
|
||||
// boundary checking result.
|
||||
InsertRangeTombstoneToMinHeap(level);
|
||||
}
|
||||
} else {
|
||||
assert(!active_.count(level));
|
||||
}
|
||||
@@ -766,13 +806,15 @@ bool MergingIterator::SkipNextDeleted() {
|
||||
// - range deletion end key
|
||||
auto current = minHeap_.top();
|
||||
if (current->type == HeapItem::DELETE_RANGE_END) {
|
||||
minHeap_.pop();
|
||||
active_.erase(current->level);
|
||||
assert(range_tombstone_iters_[current->level] &&
|
||||
range_tombstone_iters_[current->level]->Valid());
|
||||
range_tombstone_iters_[current->level]->Next();
|
||||
if (range_tombstone_iters_[current->level]->Valid()) {
|
||||
InsertRangeTombstoneToMinHeap(current->level);
|
||||
InsertRangeTombstoneToMinHeap(current->level, true /* start_key */,
|
||||
true /* replace_top */);
|
||||
} else {
|
||||
minHeap_.pop();
|
||||
}
|
||||
return true /* current key deleted */;
|
||||
}
|
||||
@@ -981,13 +1023,15 @@ bool MergingIterator::SkipPrevDeleted() {
|
||||
// - range deletion start key
|
||||
auto current = maxHeap_->top();
|
||||
if (current->type == HeapItem::DELETE_RANGE_START) {
|
||||
maxHeap_->pop();
|
||||
active_.erase(current->level);
|
||||
assert(range_tombstone_iters_[current->level] &&
|
||||
range_tombstone_iters_[current->level]->Valid());
|
||||
range_tombstone_iters_[current->level]->Prev();
|
||||
if (range_tombstone_iters_[current->level]->Valid()) {
|
||||
InsertRangeTombstoneToMaxHeap(current->level);
|
||||
InsertRangeTombstoneToMaxHeap(current->level, true /* end_key */,
|
||||
true /* replace_top */);
|
||||
} else {
|
||||
maxHeap_->pop();
|
||||
}
|
||||
return true /* current key deleted */;
|
||||
}
|
||||
@@ -1265,11 +1309,12 @@ InternalIterator* NewMergingIterator(const InternalKeyComparator* cmp,
|
||||
}
|
||||
|
||||
MergeIteratorBuilder::MergeIteratorBuilder(
|
||||
const InternalKeyComparator* comparator, Arena* a, bool prefix_seek_mode)
|
||||
const InternalKeyComparator* comparator, Arena* a, bool prefix_seek_mode,
|
||||
const Slice* iterate_upper_bound)
|
||||
: first_iter(nullptr), use_merging_iter(false), arena(a) {
|
||||
auto mem = arena->AllocateAligned(sizeof(MergingIterator));
|
||||
merge_iter =
|
||||
new (mem) MergingIterator(comparator, nullptr, 0, true, prefix_seek_mode);
|
||||
merge_iter = new (mem) MergingIterator(comparator, nullptr, 0, true,
|
||||
prefix_seek_mode, iterate_upper_bound);
|
||||
}
|
||||
|
||||
MergeIteratorBuilder::~MergeIteratorBuilder() {
|
||||
|
||||
@@ -45,7 +45,8 @@ class MergeIteratorBuilder {
|
||||
// comparator: the comparator used in merging comparator
|
||||
// arena: where the merging iterator needs to be allocated from.
|
||||
explicit MergeIteratorBuilder(const InternalKeyComparator* comparator,
|
||||
Arena* arena, bool prefix_seek_mode = false);
|
||||
Arena* arena, bool prefix_seek_mode = false,
|
||||
const Slice* iterate_upper_bound = nullptr);
|
||||
~MergeIteratorBuilder();
|
||||
|
||||
// Add iter to the merging iterator.
|
||||
|
||||
@@ -86,12 +86,6 @@ Iterator* SstFileReader::NewIterator(const ReadOptions& roptions) {
|
||||
return res;
|
||||
}
|
||||
|
||||
FragmentedRangeTombstoneIterator* SstFileReader::NewRangeTombstoneIterator(
|
||||
const ReadOptions& options) {
|
||||
auto r = rep_.get();
|
||||
return r->table_reader->NewRangeTombstoneIterator(options);
|
||||
}
|
||||
|
||||
std::shared_ptr<const TableProperties> SstFileReader::GetTableProperties()
|
||||
const {
|
||||
return rep_->table_reader->GetTableProperties();
|
||||
|
||||
@@ -385,39 +385,6 @@ TEST_F(SstFileReaderTimestampTest, Basic) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SstFileReaderTimestampTest, BasicDeleteRange) {
|
||||
SstFileWriter writer(soptions_, options_);
|
||||
ASSERT_OK(writer.Open(sst_name_));
|
||||
ASSERT_OK(writer.DeleteRange("key1", "key2", EncodeAsUint64(1)));
|
||||
ASSERT_OK(writer.Finish());
|
||||
|
||||
SstFileReader reader(options_);
|
||||
ASSERT_OK(reader.Open(sst_name_));
|
||||
ASSERT_OK(reader.VerifyChecksum());
|
||||
|
||||
ReadOptions read_options;
|
||||
std::string ts = EncodeAsUint64(2);
|
||||
Slice ts_slice = ts;
|
||||
read_options.timestamp = &ts_slice;
|
||||
FragmentedRangeTombstoneIterator* iter =
|
||||
reader.NewRangeTombstoneIterator(read_options);
|
||||
iter->SeekToFirst();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(
|
||||
StripTimestampFromUserKey(iter->start_key(), EncodeAsUint64(1).size()),
|
||||
"key1");
|
||||
ASSERT_EQ(
|
||||
StripTimestampFromUserKey(iter->end_key(), EncodeAsUint64(1).size()),
|
||||
"key2");
|
||||
ASSERT_EQ(iter->timestamp(), EncodeAsUint64(1));
|
||||
iter->Next();
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(SstFileReaderTimestampTest, TimestampsOutOfOrder) {
|
||||
SstFileWriter writer(soptions_, options_);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user