Persistent Stats: persist stats history to disk (#5046)
Summary: This PR continues the work in https://github.com/facebook/rocksdb/pull/4748 and https://github.com/facebook/rocksdb/pull/4535 by adding a new DBOption `persist_stats_to_disk` which instructs RocksDB to persist stats history to RocksDB itself. When statistics is enabled, and both options `stats_persist_period_sec` and `persist_stats_to_disk` are set, RocksDB will periodically write stats to a built-in column family in the following form: key -> (timestamp in microseconds)#(stats name), value -> stats value. The existing API `GetStatsHistory` will detect the current value of `persist_stats_to_disk` and either read from in-memory data structure or from the hidden column family on disk. Pull Request resolved: https://github.com/facebook/rocksdb/pull/5046 Differential Revision: D15863138 Pulled By: miasantreble fbshipit-source-id: bb82abdb3f2ca581aa42531734ac799f113e931bmain
parent
ee294c24ed
commit
671d15cbdd
@ -0,0 +1,171 @@ |
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include "monitoring/persistent_stats_history.h" |
||||
|
||||
#include <cstring> |
||||
#include <string> |
||||
#include <utility> |
||||
#include "db/db_impl/db_impl.h" |
||||
#include "port/likely.h" |
||||
#include "util/string_util.h" |
||||
|
||||
namespace rocksdb { |
||||
// 10 digit seconds timestamp => [Sep 9, 2001 ~ Nov 20, 2286]
|
||||
const int kNowSecondsStringLength = 10; |
||||
const std::string kFormatVersionKeyString = |
||||
"__persistent_stats_format_version__"; |
||||
const std::string kCompatibleVersionKeyString = |
||||
"__persistent_stats_compatible_version__"; |
||||
// Every release maintains two versions numbers for persistents stats: Current
|
||||
// format version and compatible format version. Current format version
|
||||
// designates what type of encoding will be used when writing to stats CF;
|
||||
// compatible format version designates the minimum format version that
|
||||
// can decode the stats CF encoded using the current format version.
|
||||
const uint64_t kStatsCFCurrentFormatVersion = 1; |
||||
const uint64_t kStatsCFCompatibleFormatVersion = 1; |
||||
|
||||
Status DecodePersistentStatsVersionNumber(DBImpl* db, StatsVersionKeyType type, |
||||
uint64_t* version_number) { |
||||
if (type >= StatsVersionKeyType::kKeyTypeMax) { |
||||
return Status::InvalidArgument("Invalid stats version key type provided"); |
||||
} |
||||
std::string key; |
||||
if (type == StatsVersionKeyType::kFormatVersion) { |
||||
key = kFormatVersionKeyString; |
||||
} else if (type == StatsVersionKeyType::kCompatibleVersion) { |
||||
key = kCompatibleVersionKeyString; |
||||
} |
||||
ReadOptions options; |
||||
options.verify_checksums = true; |
||||
std::string result; |
||||
Status s = db->Get(options, db->PersistentStatsColumnFamily(), key, &result); |
||||
if (!s.ok() || result.empty()) { |
||||
return Status::NotFound("Persistent stats version key " + key + |
||||
" not found."); |
||||
} |
||||
|
||||
// read version_number but do nothing in current version
|
||||
*version_number = ParseUint64(result); |
||||
return Status::OK(); |
||||
} |
||||
|
||||
int EncodePersistentStatsKey(uint64_t now_seconds, const std::string& key, |
||||
int size, char* buf) { |
||||
char timestamp[kNowSecondsStringLength + 1]; |
||||
// make time stamp string equal in length to allow sorting by time
|
||||
snprintf(timestamp, sizeof(timestamp), "%010d", |
||||
static_cast<int>(now_seconds)); |
||||
timestamp[kNowSecondsStringLength] = '\0'; |
||||
return snprintf(buf, size, "%s#%s", timestamp, key.c_str()); |
||||
} |
||||
|
||||
void OptimizeForPersistentStats(ColumnFamilyOptions* cfo) { |
||||
cfo->write_buffer_size = 2 << 20; |
||||
cfo->target_file_size_base = 2 * 1048576; |
||||
cfo->max_bytes_for_level_base = 10 * 1048576; |
||||
cfo->snap_refresh_nanos = 0; |
||||
cfo->soft_pending_compaction_bytes_limit = 256 * 1048576; |
||||
cfo->hard_pending_compaction_bytes_limit = 1073741824ul; |
||||
cfo->compression = kNoCompression; |
||||
} |
||||
|
||||
PersistentStatsHistoryIterator::~PersistentStatsHistoryIterator() {} |
||||
|
||||
bool PersistentStatsHistoryIterator::Valid() const { return valid_; } |
||||
|
||||
Status PersistentStatsHistoryIterator::status() const { return status_; } |
||||
|
||||
void PersistentStatsHistoryIterator::Next() { |
||||
// increment start_time by 1 to avoid infinite loop
|
||||
AdvanceIteratorByTime(GetStatsTime() + 1, end_time_); |
||||
} |
||||
|
||||
uint64_t PersistentStatsHistoryIterator::GetStatsTime() const { return time_; } |
||||
|
||||
const std::map<std::string, uint64_t>& |
||||
PersistentStatsHistoryIterator::GetStatsMap() const { |
||||
return stats_map_; |
||||
} |
||||
|
||||
std::pair<uint64_t, std::string> parseKey(const Slice& key, |
||||
uint64_t start_time) { |
||||
std::pair<uint64_t, std::string> result; |
||||
std::string key_str = key.ToString(); |
||||
std::string::size_type pos = key_str.find("#"); |
||||
// TODO(Zhongyi): add counters to track parse failures?
|
||||
if (pos == std::string::npos) { |
||||
result.first = port::kMaxUint64; |
||||
result.second.clear(); |
||||
} else { |
||||
uint64_t parsed_time = ParseUint64(key_str.substr(0, pos)); |
||||
// skip entries with timestamp smaller than start_time
|
||||
if (parsed_time < start_time) { |
||||
result.first = port::kMaxUint64; |
||||
result.second = ""; |
||||
} else { |
||||
result.first = parsed_time; |
||||
std::string key_resize = key_str.substr(pos + 1); |
||||
result.second = key_resize; |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
// advance the iterator to the next time between [start_time, end_time)
|
||||
// if success, update time_ and stats_map_ with new_time and stats_map
|
||||
void PersistentStatsHistoryIterator::AdvanceIteratorByTime(uint64_t start_time, |
||||
uint64_t end_time) { |
||||
// try to find next entry in stats_history_ map
|
||||
if (db_impl_ != nullptr) { |
||||
ReadOptions ro; |
||||
Iterator* iter = |
||||
db_impl_->NewIterator(ro, db_impl_->PersistentStatsColumnFamily()); |
||||
|
||||
char timestamp[kNowSecondsStringLength + 1]; |
||||
snprintf(timestamp, sizeof(timestamp), "%010d", |
||||
static_cast<int>(std::max(time_, start_time))); |
||||
timestamp[kNowSecondsStringLength] = '\0'; |
||||
|
||||
iter->Seek(timestamp); |
||||
// no more entries with timestamp >= start_time is found or version key
|
||||
// is found to be incompatible
|
||||
if (!iter->Valid()) { |
||||
valid_ = false; |
||||
delete iter; |
||||
return; |
||||
} |
||||
time_ = parseKey(iter->key(), start_time).first; |
||||
valid_ = true; |
||||
// check parsed time and invalid if it exceeds end_time
|
||||
if (time_ > end_time) { |
||||
valid_ = false; |
||||
delete iter; |
||||
return; |
||||
} |
||||
// find all entries with timestamp equal to time_
|
||||
std::map<std::string, uint64_t> new_stats_map; |
||||
std::pair<uint64_t, std::string> kv; |
||||
for (; iter->Valid(); iter->Next()) { |
||||
kv = parseKey(iter->key(), start_time); |
||||
if (kv.first != time_) { |
||||
break; |
||||
} |
||||
if (kv.second.compare(kFormatVersionKeyString) == 0) { |
||||
continue; |
||||
} |
||||
new_stats_map[kv.second] = ParseUint64(iter->value().ToString()); |
||||
} |
||||
stats_map_.swap(new_stats_map); |
||||
delete iter; |
||||
} else { |
||||
valid_ = false; |
||||
} |
||||
} |
||||
|
||||
} // namespace rocksdb
|
@ -0,0 +1,83 @@ |
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#pragma once |
||||
|
||||
#include "db/db_impl/db_impl.h" |
||||
#include "rocksdb/stats_history.h" |
||||
|
||||
namespace rocksdb { |
||||
|
||||
extern const std::string kFormatVersionKeyString; |
||||
extern const std::string kCompatibleVersionKeyString; |
||||
extern const uint64_t kStatsCFCurrentFormatVersion; |
||||
extern const uint64_t kStatsCFCompatibleFormatVersion; |
||||
|
||||
enum StatsVersionKeyType : uint32_t { |
||||
kFormatVersion = 1, |
||||
kCompatibleVersion = 2, |
||||
kKeyTypeMax = 3 |
||||
}; |
||||
|
||||
// Read the version number from persitent stats cf depending on type provided
|
||||
// stores the version number in `*version_number`
|
||||
// returns Status::OK() on success, or other status code on failure
|
||||
Status DecodePersistentStatsVersionNumber(DBImpl* db, StatsVersionKeyType type, |
||||
uint64_t* version_number); |
||||
|
||||
// Encode timestamp and stats key into buf
|
||||
// Format: timestamp(10 digit) + '#' + key
|
||||
// Total length of encoded key will be capped at 100 bytes
|
||||
int EncodePersistentStatsKey(uint64_t timestamp, const std::string& key, |
||||
int size, char* buf); |
||||
|
||||
void OptimizeForPersistentStats(ColumnFamilyOptions* cfo); |
||||
|
||||
class PersistentStatsHistoryIterator final : public StatsHistoryIterator { |
||||
public: |
||||
PersistentStatsHistoryIterator(uint64_t start_time, uint64_t end_time, |
||||
DBImpl* db_impl) |
||||
: time_(0), |
||||
start_time_(start_time), |
||||
end_time_(end_time), |
||||
valid_(true), |
||||
db_impl_(db_impl) { |
||||
AdvanceIteratorByTime(start_time_, end_time_); |
||||
} |
||||
~PersistentStatsHistoryIterator() override; |
||||
bool Valid() const override; |
||||
Status status() const override; |
||||
|
||||
void Next() override; |
||||
uint64_t GetStatsTime() const override; |
||||
|
||||
const std::map<std::string, uint64_t>& GetStatsMap() const override; |
||||
|
||||
private: |
||||
// advance the iterator to the next stats history record with timestamp
|
||||
// between [start_time, end_time)
|
||||
void AdvanceIteratorByTime(uint64_t start_time, uint64_t end_time); |
||||
|
||||
// No copying allowed
|
||||
PersistentStatsHistoryIterator(const PersistentStatsHistoryIterator&) = |
||||
delete; |
||||
void operator=(const PersistentStatsHistoryIterator&) = delete; |
||||
PersistentStatsHistoryIterator(PersistentStatsHistoryIterator&&) = delete; |
||||
PersistentStatsHistoryIterator& operator=(PersistentStatsHistoryIterator&&) = |
||||
delete; |
||||
|
||||
uint64_t time_; |
||||
uint64_t start_time_; |
||||
uint64_t end_time_; |
||||
std::map<std::string, uint64_t> stats_map_; |
||||
Status status_; |
||||
bool valid_; |
||||
DBImpl* db_impl_; |
||||
}; |
||||
|
||||
} // namespace rocksdb
|
@ -0,0 +1,576 @@ |
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#include <limits> |
||||
#include <string> |
||||
#include <unordered_map> |
||||
|
||||
#include "db/column_family.h" |
||||
#include "db/db_impl/db_impl.h" |
||||
#include "db/db_test_util.h" |
||||
#include "monitoring/persistent_stats_history.h" |
||||
#include "options/options_helper.h" |
||||
#include "port/stack_trace.h" |
||||
#include "rocksdb/cache.h" |
||||
#include "rocksdb/convenience.h" |
||||
#include "rocksdb/rate_limiter.h" |
||||
#include "rocksdb/stats_history.h" |
||||
#include "test_util/sync_point.h" |
||||
#include "test_util/testutil.h" |
||||
#include "util/random.h" |
||||
|
||||
namespace rocksdb { |
||||
|
||||
class StatsHistoryTest : public DBTestBase { |
||||
public: |
||||
StatsHistoryTest() : DBTestBase("/stats_history_test") {} |
||||
}; |
||||
|
||||
TEST_F(StatsHistoryTest, RunStatsDumpPeriodSec) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_dump_period_sec = 5; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
int counter = 0; |
||||
rocksdb::SyncPoint::GetInstance()->DisableProcessing(); |
||||
rocksdb::SyncPoint::GetInstance()->ClearAllCallBacks(); |
||||
#if defined(OS_MACOSX) && !defined(NDEBUG) |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"InstrumentedCondVar::TimedWaitInternal", [&](void* arg) { |
||||
uint64_t time_us = *reinterpret_cast<uint64_t*>(arg); |
||||
if (time_us < mock_env->RealNowMicros()) { |
||||
*reinterpret_cast<uint64_t*>(arg) = mock_env->RealNowMicros() + 1000; |
||||
} |
||||
}); |
||||
#endif // OS_MACOSX && !NDEBUG
|
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"DBImpl::DumpStats:1", [&](void* /*arg*/) { counter++; }); |
||||
rocksdb::SyncPoint::GetInstance()->EnableProcessing(); |
||||
Reopen(options); |
||||
ASSERT_EQ(5, dbfull()->GetDBOptions().stats_dump_period_sec); |
||||
dbfull()->TEST_WaitForDumpStatsRun([&] { mock_env->set_current_time(5); }); |
||||
ASSERT_GE(counter, 1); |
||||
|
||||
// Test cacel job through SetOptions
|
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_dump_period_sec", "0"}})); |
||||
int old_val = counter; |
||||
for (int i = 6; i < 20; ++i) { |
||||
dbfull()->TEST_WaitForDumpStatsRun([&] { mock_env->set_current_time(i); }); |
||||
} |
||||
ASSERT_EQ(counter, old_val); |
||||
Close(); |
||||
} |
||||
|
||||
// Test persistent stats background thread scheduling and cancelling
|
||||
TEST_F(StatsHistoryTest, StatsPersistScheduling) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_persist_period_sec = 5; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
rocksdb::SyncPoint::GetInstance()->DisableProcessing(); |
||||
rocksdb::SyncPoint::GetInstance()->ClearAllCallBacks(); |
||||
#if defined(OS_MACOSX) && !defined(NDEBUG) |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"InstrumentedCondVar::TimedWaitInternal", [&](void* arg) { |
||||
uint64_t time_us = *reinterpret_cast<uint64_t*>(arg); |
||||
if (time_us < mock_env->RealNowMicros()) { |
||||
*reinterpret_cast<uint64_t*>(arg) = mock_env->RealNowMicros() + 1000; |
||||
} |
||||
}); |
||||
#endif // OS_MACOSX && !NDEBUG
|
||||
int counter = 0; |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"DBImpl::PersistStats:Entry", [&](void* /*arg*/) { counter++; }); |
||||
rocksdb::SyncPoint::GetInstance()->EnableProcessing(); |
||||
Reopen(options); |
||||
ASSERT_EQ(5, dbfull()->GetDBOptions().stats_persist_period_sec); |
||||
dbfull()->TEST_WaitForPersistStatsRun([&] { mock_env->set_current_time(5); }); |
||||
ASSERT_GE(counter, 1); |
||||
|
||||
// Test cacel job through SetOptions
|
||||
ASSERT_TRUE(dbfull()->TEST_IsPersistentStatsEnabled()); |
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_persist_period_sec", "0"}})); |
||||
ASSERT_FALSE(dbfull()->TEST_IsPersistentStatsEnabled()); |
||||
Close(); |
||||
} |
||||
|
||||
// Test enabling persistent stats for the first time
|
||||
TEST_F(StatsHistoryTest, PersistentStatsFreshInstall) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_persist_period_sec = 0; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
rocksdb::SyncPoint::GetInstance()->DisableProcessing(); |
||||
rocksdb::SyncPoint::GetInstance()->ClearAllCallBacks(); |
||||
#if defined(OS_MACOSX) && !defined(NDEBUG) |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"InstrumentedCondVar::TimedWaitInternal", [&](void* arg) { |
||||
uint64_t time_us = *reinterpret_cast<uint64_t*>(arg); |
||||
if (time_us < mock_env->RealNowMicros()) { |
||||
*reinterpret_cast<uint64_t*>(arg) = mock_env->RealNowMicros() + 1000; |
||||
} |
||||
}); |
||||
#endif // OS_MACOSX && !NDEBUG
|
||||
int counter = 0; |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"DBImpl::PersistStats:Entry", [&](void* /*arg*/) { counter++; }); |
||||
rocksdb::SyncPoint::GetInstance()->EnableProcessing(); |
||||
Reopen(options); |
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_persist_period_sec", "5"}})); |
||||
ASSERT_EQ(5, dbfull()->GetDBOptions().stats_persist_period_sec); |
||||
dbfull()->TEST_WaitForPersistStatsRun([&] { mock_env->set_current_time(5); }); |
||||
ASSERT_GE(counter, 1); |
||||
Close(); |
||||
} |
||||
|
||||
// TODO(Zhongyi): Move persistent stats related tests to a separate file
|
||||
TEST_F(StatsHistoryTest, GetStatsHistoryInMemory) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_persist_period_sec = 5; |
||||
options.statistics = rocksdb::CreateDBStatistics(); |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
#if defined(OS_MACOSX) && !defined(NDEBUG) |
||||
rocksdb::SyncPoint::GetInstance()->DisableProcessing(); |
||||
rocksdb::SyncPoint::GetInstance()->ClearAllCallBacks(); |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"InstrumentedCondVar::TimedWaitInternal", [&](void* arg) { |
||||
uint64_t time_us = *reinterpret_cast<uint64_t*>(arg); |
||||
if (time_us < mock_env->RealNowMicros()) { |
||||
*reinterpret_cast<uint64_t*>(arg) = mock_env->RealNowMicros() + 1000; |
||||
} |
||||
}); |
||||
rocksdb::SyncPoint::GetInstance()->EnableProcessing(); |
||||
#endif // OS_MACOSX && !NDEBUG
|
||||
|
||||
CreateColumnFamilies({"pikachu"}, options); |
||||
ASSERT_OK(Put("foo", "bar")); |
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options); |
||||
|
||||
int mock_time = 1; |
||||
// Wait for stats persist to finish
|
||||
dbfull()->TEST_WaitForPersistStatsRun([&] { mock_env->set_current_time(5); }); |
||||
std::unique_ptr<StatsHistoryIterator> stats_iter; |
||||
db_->GetStatsHistory(0 /*start_time*/, 6 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
// disabled stats snapshots
|
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_persist_period_sec", "0"}})); |
||||
size_t stats_count = 0; |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
ASSERT_EQ(stats_iter->GetStatsTime(), 5); |
||||
stats_count += stats_map.size(); |
||||
} |
||||
ASSERT_GT(stats_count, 0); |
||||
// Wait a bit and verify no more stats are found
|
||||
for (mock_time = 6; mock_time < 20; ++mock_time) { |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(mock_time); }); |
||||
} |
||||
db_->GetStatsHistory(0 /*start_time*/, 20 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
size_t stats_count_new = 0; |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
stats_count_new += stats_iter->GetStatsMap().size(); |
||||
} |
||||
ASSERT_EQ(stats_count_new, stats_count); |
||||
Close(); |
||||
} |
||||
|
||||
TEST_F(StatsHistoryTest, InMemoryStatsHistoryPurging) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.statistics = rocksdb::CreateDBStatistics(); |
||||
options.stats_persist_period_sec = 1; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
#if defined(OS_MACOSX) && !defined(NDEBUG) |
||||
rocksdb::SyncPoint::GetInstance()->DisableProcessing(); |
||||
rocksdb::SyncPoint::GetInstance()->ClearAllCallBacks(); |
||||
rocksdb::SyncPoint::GetInstance()->SetCallBack( |
||||
"InstrumentedCondVar::TimedWaitInternal", [&](void* arg) { |
||||
uint64_t time_us = *reinterpret_cast<uint64_t*>(arg); |
||||
if (time_us < mock_env->RealNowMicros()) { |
||||
*reinterpret_cast<uint64_t*>(arg) = mock_env->RealNowMicros() + 1000; |
||||
} |
||||
}); |
||||
rocksdb::SyncPoint::GetInstance()->EnableProcessing(); |
||||
#endif // OS_MACOSX && !NDEBUG
|
||||
|
||||
CreateColumnFamilies({"pikachu"}, options); |
||||
ASSERT_OK(Put("foo", "bar")); |
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options); |
||||
// some random operation to populate statistics
|
||||
ASSERT_OK(Delete("foo")); |
||||
ASSERT_OK(Put("sol", "sol")); |
||||
ASSERT_OK(Put("epic", "epic")); |
||||
ASSERT_OK(Put("ltd", "ltd")); |
||||
ASSERT_EQ("sol", Get("sol")); |
||||
ASSERT_EQ("epic", Get("epic")); |
||||
ASSERT_EQ("ltd", Get("ltd")); |
||||
Iterator* iterator = db_->NewIterator(ReadOptions()); |
||||
for (iterator->SeekToFirst(); iterator->Valid(); iterator->Next()) { |
||||
ASSERT_TRUE(iterator->key() == iterator->value()); |
||||
} |
||||
delete iterator; |
||||
ASSERT_OK(Flush()); |
||||
ASSERT_OK(Delete("sol")); |
||||
db_->CompactRange(CompactRangeOptions(), nullptr, nullptr); |
||||
int mock_time = 1; |
||||
// Wait for stats persist to finish
|
||||
for (; mock_time < 5; ++mock_time) { |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(mock_time); }); |
||||
} |
||||
|
||||
// second round of ops
|
||||
ASSERT_OK(Put("saigon", "saigon")); |
||||
ASSERT_OK(Put("noodle talk", "noodle talk")); |
||||
ASSERT_OK(Put("ping bistro", "ping bistro")); |
||||
iterator = db_->NewIterator(ReadOptions()); |
||||
for (iterator->SeekToFirst(); iterator->Valid(); iterator->Next()) { |
||||
ASSERT_TRUE(iterator->key() == iterator->value()); |
||||
} |
||||
delete iterator; |
||||
ASSERT_OK(Flush()); |
||||
db_->CompactRange(CompactRangeOptions(), nullptr, nullptr); |
||||
for (; mock_time < 10; ++mock_time) { |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(mock_time); }); |
||||
} |
||||
std::unique_ptr<StatsHistoryIterator> stats_iter; |
||||
db_->GetStatsHistory(0 /*start_time*/, 10 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
size_t stats_count = 0; |
||||
int slice_count = 0; |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
slice_count++; |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
stats_count += stats_map.size(); |
||||
} |
||||
size_t stats_history_size = dbfull()->TEST_EstimateInMemoryStatsHistorySize(); |
||||
ASSERT_GE(slice_count, 9); |
||||
ASSERT_GE(stats_history_size, 12000); |
||||
// capping memory cost at 12000 bytes since one slice is around 10000~12000
|
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_history_buffer_size", "12000"}})); |
||||
ASSERT_EQ(12000, dbfull()->GetDBOptions().stats_history_buffer_size); |
||||
// Wait for stats persist to finish
|
||||
for (; mock_time < 20; ++mock_time) { |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(mock_time); }); |
||||
} |
||||
db_->GetStatsHistory(0 /*start_time*/, 20 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
size_t stats_count_reopen = 0; |
||||
slice_count = 0; |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
slice_count++; |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
stats_count_reopen += stats_map.size(); |
||||
} |
||||
size_t stats_history_size_reopen = |
||||
dbfull()->TEST_EstimateInMemoryStatsHistorySize(); |
||||
// only one slice can fit under the new stats_history_buffer_size
|
||||
ASSERT_LT(slice_count, 2); |
||||
ASSERT_TRUE(stats_history_size_reopen < 12000 && |
||||
stats_history_size_reopen > 0); |
||||
ASSERT_TRUE(stats_count_reopen < stats_count && stats_count_reopen > 0); |
||||
Close(); |
||||
// TODO: may also want to verify stats timestamp to make sure we are purging
|
||||
// the correct stats snapshot
|
||||
} |
||||
|
||||
int countkeys(Iterator* iter) { |
||||
int count = 0; |
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { |
||||
count++; |
||||
} |
||||
return count; |
||||
} |
||||
|
||||
TEST_F(StatsHistoryTest, GetStatsHistoryFromDisk) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_persist_period_sec = 5; |
||||
options.statistics = rocksdb::CreateDBStatistics(); |
||||
options.persist_stats_to_disk = true; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
CreateColumnFamilies({"pikachu"}, options); |
||||
ASSERT_OK(Put("foo", "bar")); |
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options); |
||||
ASSERT_EQ(Get("foo"), "bar"); |
||||
|
||||
// Wait for stats persist to finish
|
||||
dbfull()->TEST_WaitForPersistStatsRun([&] { mock_env->set_current_time(5); }); |
||||
auto iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
int key_count1 = countkeys(iter); |
||||
delete iter; |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(10); }); |
||||
iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
int key_count2 = countkeys(iter); |
||||
delete iter; |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(15); }); |
||||
iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
int key_count3 = countkeys(iter); |
||||
delete iter; |
||||
ASSERT_GE(key_count2, key_count1); |
||||
ASSERT_GE(key_count3, key_count2); |
||||
ASSERT_EQ(key_count3 - key_count2, key_count2 - key_count1); |
||||
std::unique_ptr<StatsHistoryIterator> stats_iter; |
||||
db_->GetStatsHistory(0 /*start_time*/, 16 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
size_t stats_count = 0; |
||||
int slice_count = 0; |
||||
int non_zero_count = 0; |
||||
for (int i = 1; stats_iter->Valid(); stats_iter->Next(), i++) { |
||||
slice_count++; |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
ASSERT_EQ(stats_iter->GetStatsTime(), 5 * i); |
||||
for (auto& stat : stats_map) { |
||||
if (stat.second != 0) { |
||||
non_zero_count++; |
||||
} |
||||
} |
||||
stats_count += stats_map.size(); |
||||
} |
||||
ASSERT_EQ(slice_count, 3); |
||||
// 2 extra keys for format version
|
||||
ASSERT_EQ(stats_count, key_count3 - 2); |
||||
// verify reopen will not cause data loss
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options); |
||||
db_->GetStatsHistory(0 /*start_time*/, 16 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
size_t stats_count_reopen = 0; |
||||
int slice_count_reopen = 0; |
||||
int non_zero_count_recover = 0; |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
slice_count_reopen++; |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
for (auto& stat : stats_map) { |
||||
if (stat.second != 0) { |
||||
non_zero_count_recover++; |
||||
} |
||||
} |
||||
stats_count_reopen += stats_map.size(); |
||||
} |
||||
ASSERT_EQ(non_zero_count, non_zero_count_recover); |
||||
ASSERT_EQ(slice_count, slice_count_reopen); |
||||
ASSERT_EQ(stats_count, stats_count_reopen); |
||||
Close(); |
||||
} |
||||
|
||||
// Test persisted stats matches the value found in options.statistics and
|
||||
// the stats value retains after DB reopen
|
||||
TEST_F(StatsHistoryTest, PersitentStatsVerifyValue) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_persist_period_sec = 5; |
||||
options.statistics = rocksdb::CreateDBStatistics(); |
||||
options.persist_stats_to_disk = true; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
std::map<std::string, uint64_t> stats_map_before; |
||||
ASSERT_TRUE(options.statistics->getTickerMap(&stats_map_before)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
CreateColumnFamilies({"pikachu"}, options); |
||||
ASSERT_OK(Put("foo", "bar")); |
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options); |
||||
ASSERT_EQ(Get("foo"), "bar"); |
||||
|
||||
// Wait for stats persist to finish
|
||||
dbfull()->TEST_WaitForPersistStatsRun([&] { mock_env->set_current_time(5); }); |
||||
auto iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
countkeys(iter); |
||||
delete iter; |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(10); }); |
||||
iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
countkeys(iter); |
||||
delete iter; |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(15); }); |
||||
iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
countkeys(iter); |
||||
delete iter; |
||||
dbfull()->TEST_WaitForPersistStatsRun( |
||||
[&] { mock_env->set_current_time(20); }); |
||||
|
||||
std::map<std::string, uint64_t> stats_map_after; |
||||
ASSERT_TRUE(options.statistics->getTickerMap(&stats_map_after)); |
||||
std::unique_ptr<StatsHistoryIterator> stats_iter; |
||||
db_->GetStatsHistory(0 /*start_time*/, 21 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
std::string sample = "rocksdb.num.iterator.deleted"; |
||||
uint64_t recovered_value = 0; |
||||
for (int i = 1; stats_iter->Valid(); stats_iter->Next(), ++i) { |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
ASSERT_EQ(stats_iter->GetStatsTime(), 5 * i); |
||||
for (const auto& stat : stats_map) { |
||||
if (sample.compare(stat.first) == 0) { |
||||
recovered_value += stat.second; |
||||
} |
||||
} |
||||
} |
||||
ASSERT_EQ(recovered_value, stats_map_after[sample]); |
||||
|
||||
// test stats value retains after recovery
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options); |
||||
db_->GetStatsHistory(0 /*start_time*/, 21 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
uint64_t new_recovered_value = 0; |
||||
for (int i = 1; stats_iter->Valid(); stats_iter->Next(), i++) { |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
ASSERT_EQ(stats_iter->GetStatsTime(), 5 * i); |
||||
for (const auto& stat : stats_map) { |
||||
if (sample.compare(stat.first) == 0) { |
||||
new_recovered_value += stat.second; |
||||
} |
||||
} |
||||
} |
||||
ASSERT_EQ(recovered_value, new_recovered_value); |
||||
|
||||
// TODO(Zhongyi): also add test to read raw values from disk and verify
|
||||
// correctness
|
||||
Close(); |
||||
} |
||||
|
||||
// TODO(Zhongyi): add test for different format versions
|
||||
|
||||
TEST_F(StatsHistoryTest, PersistentStatsCreateColumnFamilies) { |
||||
Options options; |
||||
options.create_if_missing = true; |
||||
options.stats_persist_period_sec = 5; |
||||
options.statistics = rocksdb::CreateDBStatistics(); |
||||
options.persist_stats_to_disk = true; |
||||
std::unique_ptr<rocksdb::MockTimeEnv> mock_env; |
||||
mock_env.reset(new rocksdb::MockTimeEnv(env_)); |
||||
mock_env->set_current_time(0); // in seconds
|
||||
options.env = mock_env.get(); |
||||
ASSERT_OK(TryReopen(options)); |
||||
CreateColumnFamilies({"one", "two", "three"}, options); |
||||
ASSERT_OK(Put(1, "foo", "bar")); |
||||
ReopenWithColumnFamilies({"default", "one", "two", "three"}, options); |
||||
ASSERT_EQ(Get(2, "foo"), "bar"); |
||||
CreateColumnFamilies({"four"}, options); |
||||
ReopenWithColumnFamilies({"default", "one", "two", "three", "four"}, options); |
||||
ASSERT_EQ(Get(2, "foo"), "bar"); |
||||
dbfull()->TEST_WaitForPersistStatsRun([&] { mock_env->set_current_time(5); }); |
||||
auto iter = |
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily()); |
||||
int key_count = countkeys(iter); |
||||
delete iter; |
||||
ASSERT_GE(key_count, 0); |
||||
uint64_t num_write_wal = 0; |
||||
std::string sample = "rocksdb.write.wal"; |
||||
std::unique_ptr<StatsHistoryIterator> stats_iter; |
||||
db_->GetStatsHistory(0 /*start_time*/, 5 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
for (const auto& stat : stats_map) { |
||||
if (sample.compare(stat.first) == 0) { |
||||
num_write_wal += stat.second; |
||||
} |
||||
} |
||||
} |
||||
stats_iter.reset(); |
||||
ASSERT_EQ(num_write_wal, 2); |
||||
|
||||
options.persist_stats_to_disk = false; |
||||
ReopenWithColumnFamilies({"default", "one", "two", "three", "four"}, options); |
||||
int cf_count = 0; |
||||
for (auto cfd : *dbfull()->versions_->GetColumnFamilySet()) { |
||||
(void)cfd; |
||||
cf_count++; |
||||
} |
||||
// persistent stats cf will be implicitly opened even if
|
||||
// persist_stats_to_disk is false
|
||||
ASSERT_EQ(cf_count, 6); |
||||
ASSERT_EQ(Get(2, "foo"), "bar"); |
||||
|
||||
// attempt to create column family using same name, should fail
|
||||
ColumnFamilyOptions cf_opts(options); |
||||
ColumnFamilyHandle* handle; |
||||
ASSERT_NOK(db_->CreateColumnFamily(cf_opts, kPersistentStatsColumnFamilyName, |
||||
&handle)); |
||||
|
||||
options.persist_stats_to_disk = true; |
||||
ReopenWithColumnFamilies({"default", "one", "two", "three", "four"}, options); |
||||
ASSERT_NOK(db_->CreateColumnFamily(cf_opts, kPersistentStatsColumnFamilyName, |
||||
&handle)); |
||||
// verify stats is not affected by prior failed CF creation
|
||||
db_->GetStatsHistory(0 /*start_time*/, 5 /*end_time*/, &stats_iter); |
||||
ASSERT_TRUE(stats_iter != nullptr); |
||||
num_write_wal = 0; |
||||
for (; stats_iter->Valid(); stats_iter->Next()) { |
||||
auto stats_map = stats_iter->GetStatsMap(); |
||||
for (const auto& stat : stats_map) { |
||||
if (sample.compare(stat.first) == 0) { |
||||
num_write_wal += stat.second; |
||||
} |
||||
} |
||||
} |
||||
ASSERT_EQ(num_write_wal, 2); |
||||
|
||||
Close(); |
||||
Destroy(options); |
||||
} |
||||
|
||||
TEST_F(StatsHistoryTest, PersistentStatsReadOnly) { |
||||
ASSERT_OK(Put("bar", "v2")); |
||||
Close(); |
||||
|
||||
auto options = CurrentOptions(); |
||||
options.stats_persist_period_sec = 5; |
||||
options.persist_stats_to_disk = true; |
||||
assert(options.env == env_); |
||||
ASSERT_OK(ReadOnlyReopen(options)); |
||||
ASSERT_EQ("v2", Get("bar")); |
||||
Close(); |
||||
|
||||
// Reopen and flush memtable.
|
||||
Reopen(options); |
||||
Flush(); |
||||
Close(); |
||||
// Now check keys in read only mode.
|
||||
ASSERT_OK(ReadOnlyReopen(options)); |
||||
} |
||||
|
||||
} // namespace rocksdb
|
||||
|
||||
int main(int argc, char** argv) { |
||||
rocksdb::port::InstallStackTraceHandler(); |
||||
::testing::InitGoogleTest(&argc, argv); |
||||
return RUN_ALL_TESTS(); |
||||
} |
Loading…
Reference in new issue