// 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 <sys/types.h>
|
|
|
|
#include "db/db_impl.h"
|
|
#include "db/filename.h"
|
|
#include "db/log_format.h"
|
|
#include "db/version_set.h"
|
|
#include "leveldb/cache.h"
|
|
#include "leveldb/db.h"
|
|
#include "leveldb/table.h"
|
|
#include "leveldb/write_batch.h"
|
|
#include "util/logging.h"
|
|
#include "util/testharness.h"
|
|
#include "util/testutil.h"
|
|
|
|
namespace leveldb {
|
|
|
|
static const int kValueSize = 1000;
|
|
|
|
class CorruptionTest {
|
|
public:
|
|
CorruptionTest()
|
|
: db_(nullptr),
|
|
dbname_("/memenv/corruption_test"),
|
|
tiny_cache_(NewLRUCache(100)) {
|
|
options_.env = &env_;
|
|
options_.block_cache = tiny_cache_;
|
|
DestroyDB(dbname_, options_);
|
|
|
|
options_.create_if_missing = true;
|
|
Reopen();
|
|
options_.create_if_missing = false;
|
|
}
|
|
|
|
~CorruptionTest() {
|
|
delete db_;
|
|
delete tiny_cache_;
|
|
}
|
|
|
|
Status TryReopen() {
|
|
delete db_;
|
|
db_ = nullptr;
|
|
return DB::Open(options_, dbname_, &db_);
|
|
}
|
|
|
|
void Reopen() { ASSERT_OK(TryReopen()); }
|
|
|
|
void RepairDB() {
|
|
delete db_;
|
|
db_ = nullptr;
|
|
ASSERT_OK(::leveldb::RepairDB(dbname_, options_));
|
|
}
|
|
|
|
void Build(int n) {
|
|
std::string key_space, value_space;
|
|
WriteBatch batch;
|
|
for (int i = 0; i < n; i++) {
|
|
// if ((i % 100) == 0) fprintf(stderr, "@ %d of %d\n", i, n);
|
|
Slice key = Key(i, &key_space);
|
|
batch.Clear();
|
|
batch.Put(key, Value(i, &value_space));
|
|
WriteOptions options;
|
|
// Corrupt() doesn't work without this sync on windows; stat reports 0 for
|
|
// the file size.
|
|
if (i == n - 1) {
|
|
options.sync = true;
|
|
}
|
|
ASSERT_OK(db_->Write(options, &batch));
|
|
}
|
|
}
|
|
|
|
void Check(int min_expected, int max_expected) {
|
|
int next_expected = 0;
|
|
int missed = 0;
|
|
int bad_keys = 0;
|
|
int bad_values = 0;
|
|
int correct = 0;
|
|
std::string value_space;
|
|
Iterator* iter = db_->NewIterator(ReadOptions());
|
|
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
|
uint64_t key;
|
|
Slice in(iter->key());
|
|
if (in == "" || in == "~") {
|
|
// Ignore boundary keys.
|
|
continue;
|
|
}
|
|
if (!ConsumeDecimalNumber(&in, &key) || !in.empty() ||
|
|
key < next_expected) {
|
|
bad_keys++;
|
|
continue;
|
|
}
|
|
missed += (key - next_expected);
|
|
next_expected = key + 1;
|
|
if (iter->value() != Value(key, &value_space)) {
|
|
bad_values++;
|
|
} else {
|
|
correct++;
|
|
}
|
|
}
|
|
delete iter;
|
|
|
|
fprintf(stderr,
|
|
"expected=%d..%d; got=%d; bad_keys=%d; bad_values=%d; missed=%d\n",
|
|
min_expected, max_expected, correct, bad_keys, bad_values, missed);
|
|
ASSERT_LE(min_expected, correct);
|
|
ASSERT_GE(max_expected, correct);
|
|
}
|
|
|
|
void Corrupt(FileType filetype, int offset, int bytes_to_corrupt) {
|
|
// Pick file to corrupt
|
|
std::vector<std::string> filenames;
|
|
ASSERT_OK(env_.target()->GetChildren(dbname_, &filenames));
|
|
uint64_t number;
|
|
FileType type;
|
|
std::string fname;
|
|
int picked_number = -1;
|
|
for (size_t i = 0; i < filenames.size(); i++) {
|
|
if (ParseFileName(filenames[i], &number, &type) && type == filetype &&
|
|
int(number) > picked_number) { // Pick latest file
|
|
fname = dbname_ + "/" + filenames[i];
|
|
picked_number = number;
|
|
}
|
|
}
|
|
ASSERT_TRUE(!fname.empty()) << filetype;
|
|
|
|
uint64_t file_size;
|
|
ASSERT_OK(env_.target()->GetFileSize(fname, &file_size));
|
|
|
|
if (offset < 0) {
|
|
// Relative to end of file; make it absolute
|
|
if (-offset > file_size) {
|
|
offset = 0;
|
|
} else {
|
|
offset = file_size + offset;
|
|
}
|
|
}
|
|
if (offset > file_size) {
|
|
offset = file_size;
|
|
}
|
|
if (offset + bytes_to_corrupt > file_size) {
|
|
bytes_to_corrupt = file_size - offset;
|
|
}
|
|
|
|
// Do it
|
|
std::string contents;
|
|
Status s = ReadFileToString(env_.target(), fname, &contents);
|
|
ASSERT_TRUE(s.ok()) << s.ToString();
|
|
for (int i = 0; i < bytes_to_corrupt; i++) {
|
|
contents[i + offset] ^= 0x80;
|
|
}
|
|
s = WriteStringToFile(env_.target(), contents, fname);
|
|
ASSERT_TRUE(s.ok()) << s.ToString();
|
|
}
|
|
|
|
int Property(const std::string& name) {
|
|
std::string property;
|
|
int result;
|
|
if (db_->GetProperty(name, &property) &&
|
|
sscanf(property.c_str(), "%d", &result) == 1) {
|
|
return result;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Return the ith key
|
|
Slice Key(int i, std::string* storage) {
|
|
char buf[100];
|
|
snprintf(buf, sizeof(buf), "%016d", i);
|
|
storage->assign(buf, strlen(buf));
|
|
return Slice(*storage);
|
|
}
|
|
|
|
// Return the value to associate with the specified key
|
|
Slice Value(int k, std::string* storage) {
|
|
Random r(k);
|
|
return test::RandomString(&r, kValueSize, storage);
|
|
}
|
|
|
|
test::ErrorEnv env_;
|
|
Options options_;
|
|
DB* db_;
|
|
|
|
private:
|
|
std::string dbname_;
|
|
Cache* tiny_cache_;
|
|
};
|
|
|
|
TEST(CorruptionTest, Recovery) {
|
|
Build(100);
|
|
Check(100, 100);
|
|
Corrupt(kLogFile, 19, 1); // WriteBatch tag for first record
|
|
Corrupt(kLogFile, log::kBlockSize + 1000, 1); // Somewhere in second block
|
|
Reopen();
|
|
|
|
// The 64 records in the first two log blocks are completely lost.
|
|
Check(36, 36);
|
|
}
|
|
|
|
TEST(CorruptionTest, RecoverWriteError) {
|
|
env_.writable_file_error_ = true;
|
|
Status s = TryReopen();
|
|
ASSERT_TRUE(!s.ok());
|
|
}
|
|
|
|
TEST(CorruptionTest, NewFileErrorDuringWrite) {
|
|
// Do enough writing to force minor compaction
|
|
env_.writable_file_error_ = true;
|
|
const int num = 3 + (Options().write_buffer_size / kValueSize);
|
|
std::string value_storage;
|
|
Status s;
|
|
for (int i = 0; s.ok() && i < num; i++) {
|
|
WriteBatch batch;
|
|
batch.Put("a", Value(100, &value_storage));
|
|
s = db_->Write(WriteOptions(), &batch);
|
|
}
|
|
ASSERT_TRUE(!s.ok());
|
|
ASSERT_GE(env_.num_writable_file_errors_, 1);
|
|
env_.writable_file_error_ = false;
|
|
Reopen();
|
|
}
|
|
|
|
TEST(CorruptionTest, TableFile) {
|
|
Build(100);
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
dbi->TEST_CompactMemTable();
|
|
dbi->TEST_CompactRange(0, nullptr, nullptr);
|
|
dbi->TEST_CompactRange(1, nullptr, nullptr);
|
|
|
|
Corrupt(kTableFile, 100, 1);
|
|
Check(90, 99);
|
|
}
|
|
|
|
TEST(CorruptionTest, TableFileRepair) {
|
|
options_.block_size = 2 * kValueSize; // Limit scope of corruption
|
|
options_.paranoid_checks = true;
|
|
Reopen();
|
|
Build(100);
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
dbi->TEST_CompactMemTable();
|
|
dbi->TEST_CompactRange(0, nullptr, nullptr);
|
|
dbi->TEST_CompactRange(1, nullptr, nullptr);
|
|
|
|
Corrupt(kTableFile, 100, 1);
|
|
RepairDB();
|
|
Reopen();
|
|
Check(95, 99);
|
|
}
|
|
|
|
TEST(CorruptionTest, TableFileIndexData) {
|
|
Build(10000); // Enough to build multiple Tables
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
dbi->TEST_CompactMemTable();
|
|
|
|
Corrupt(kTableFile, -2000, 500);
|
|
Reopen();
|
|
Check(5000, 9999);
|
|
}
|
|
|
|
TEST(CorruptionTest, MissingDescriptor) {
|
|
Build(1000);
|
|
RepairDB();
|
|
Reopen();
|
|
Check(1000, 1000);
|
|
}
|
|
|
|
TEST(CorruptionTest, SequenceNumberRecovery) {
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "v1"));
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "v2"));
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "v3"));
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "v4"));
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "v5"));
|
|
RepairDB();
|
|
Reopen();
|
|
std::string v;
|
|
ASSERT_OK(db_->Get(ReadOptions(), "foo", &v));
|
|
ASSERT_EQ("v5", v);
|
|
// Write something. If sequence number was not recovered properly,
|
|
// it will be hidden by an earlier write.
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "v6"));
|
|
ASSERT_OK(db_->Get(ReadOptions(), "foo", &v));
|
|
ASSERT_EQ("v6", v);
|
|
Reopen();
|
|
ASSERT_OK(db_->Get(ReadOptions(), "foo", &v));
|
|
ASSERT_EQ("v6", v);
|
|
}
|
|
|
|
TEST(CorruptionTest, CorruptedDescriptor) {
|
|
ASSERT_OK(db_->Put(WriteOptions(), "foo", "hello"));
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
dbi->TEST_CompactMemTable();
|
|
dbi->TEST_CompactRange(0, nullptr, nullptr);
|
|
|
|
Corrupt(kDescriptorFile, 0, 1000);
|
|
Status s = TryReopen();
|
|
ASSERT_TRUE(!s.ok());
|
|
|
|
RepairDB();
|
|
Reopen();
|
|
std::string v;
|
|
ASSERT_OK(db_->Get(ReadOptions(), "foo", &v));
|
|
ASSERT_EQ("hello", v);
|
|
}
|
|
|
|
TEST(CorruptionTest, CompactionInputError) {
|
|
Build(10);
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
dbi->TEST_CompactMemTable();
|
|
const int last = config::kMaxMemCompactLevel;
|
|
ASSERT_EQ(1, Property("leveldb.num-files-at-level" + NumberToString(last)));
|
|
|
|
Corrupt(kTableFile, 100, 1);
|
|
Check(5, 9);
|
|
|
|
// Force compactions by writing lots of values
|
|
Build(10000);
|
|
Check(10000, 10000);
|
|
}
|
|
|
|
TEST(CorruptionTest, CompactionInputErrorParanoid) {
|
|
options_.paranoid_checks = true;
|
|
options_.write_buffer_size = 512 << 10;
|
|
Reopen();
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
|
|
// Make multiple inputs so we need to compact.
|
|
for (int i = 0; i < 2; i++) {
|
|
Build(10);
|
|
dbi->TEST_CompactMemTable();
|
|
Corrupt(kTableFile, 100, 1);
|
|
env_.SleepForMicroseconds(100000);
|
|
}
|
|
dbi->CompactRange(nullptr, nullptr);
|
|
|
|
// Write must fail because of corrupted table
|
|
std::string tmp1, tmp2;
|
|
Status s = db_->Put(WriteOptions(), Key(5, &tmp1), Value(5, &tmp2));
|
|
ASSERT_TRUE(!s.ok()) << "write did not fail in corrupted paranoid db";
|
|
}
|
|
|
|
TEST(CorruptionTest, UnrelatedKeys) {
|
|
Build(10);
|
|
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
|
|
dbi->TEST_CompactMemTable();
|
|
Corrupt(kTableFile, 100, 1);
|
|
|
|
std::string tmp1, tmp2;
|
|
ASSERT_OK(db_->Put(WriteOptions(), Key(1000, &tmp1), Value(1000, &tmp2)));
|
|
std::string v;
|
|
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
|
|
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
|
|
dbi->TEST_CompactMemTable();
|
|
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
|
|
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
|
|
}
|
|
|
|
} // namespace leveldb
|
|
|
|
int main(int argc, char** argv) { return leveldb::test::RunAllTests(); }
|