2016-02-09 23:12:00 +00:00
|
|
|
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
2017-07-15 23:03:42 +00:00
|
|
|
// 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).
|
2015-01-15 18:28:10 +00:00
|
|
|
//
|
|
|
|
// Copyright 2014 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.
|
|
|
|
|
|
|
|
// This test uses a custom Env to keep track of the state of a filesystem as of
|
|
|
|
// the last "sync". It then checks for data loss errors by purposely dropping
|
|
|
|
// file data (or entire files) not protected by a "sync".
|
|
|
|
|
2019-05-31 22:21:36 +00:00
|
|
|
#include "db/db_impl/db_impl.h"
|
2015-01-15 18:28:10 +00:00
|
|
|
#include "db/log_format.h"
|
|
|
|
#include "db/version_set.h"
|
2017-04-06 02:02:00 +00:00
|
|
|
#include "env/mock_env.h"
|
2019-05-30 03:44:08 +00:00
|
|
|
#include "file/filename.h"
|
2019-06-01 00:19:43 +00:00
|
|
|
#include "logging/logging.h"
|
2015-01-15 18:28:10 +00:00
|
|
|
#include "rocksdb/cache.h"
|
|
|
|
#include "rocksdb/db.h"
|
|
|
|
#include "rocksdb/env.h"
|
|
|
|
#include "rocksdb/table.h"
|
|
|
|
#include "rocksdb/write_batch.h"
|
2019-05-30 18:21:38 +00:00
|
|
|
#include "test_util/fault_injection_test_env.h"
|
|
|
|
#include "test_util/sync_point.h"
|
|
|
|
#include "test_util/testharness.h"
|
|
|
|
#include "test_util/testutil.h"
|
2019-05-31 00:39:43 +00:00
|
|
|
#include "util/mutexlock.h"
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2020-02-20 20:07:53 +00:00
|
|
|
namespace ROCKSDB_NAMESPACE {
|
2015-01-15 18:28:10 +00:00
|
|
|
|
|
|
|
static const int kValueSize = 1000;
|
|
|
|
static const int kMaxNumValues = 2000;
|
|
|
|
static const size_t kNumIterations = 3;
|
|
|
|
|
2018-05-07 19:15:54 +00:00
|
|
|
enum FaultInjectionOptionConfig {
|
|
|
|
kDefault,
|
|
|
|
kDifferentDataDir,
|
|
|
|
kWalDir,
|
|
|
|
kSyncWal,
|
|
|
|
kWalDirSyncWal,
|
|
|
|
kMultiLevels,
|
|
|
|
kEnd,
|
|
|
|
};
|
|
|
|
class FaultInjectionTest
|
|
|
|
: public testing::Test,
|
|
|
|
public testing::WithParamInterface<std::tuple<
|
|
|
|
bool, FaultInjectionOptionConfig, FaultInjectionOptionConfig>> {
|
2015-01-23 02:34:23 +00:00
|
|
|
protected:
|
|
|
|
int option_config_;
|
2018-05-07 19:15:54 +00:00
|
|
|
int non_inclusive_end_range_; // kEnd or equivalent to that
|
2015-01-23 02:34:23 +00:00
|
|
|
// When need to make sure data is persistent, sync WAL
|
2015-01-24 00:03:24 +00:00
|
|
|
bool sync_use_wal_;
|
2015-01-23 02:34:23 +00:00
|
|
|
// When need to make sure data is persistent, call DB::CompactRange()
|
2015-01-24 00:03:24 +00:00
|
|
|
bool sync_use_compact_;
|
2015-01-23 02:34:23 +00:00
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
bool sequential_order_;
|
|
|
|
|
2015-01-23 02:34:23 +00:00
|
|
|
protected:
|
2015-01-15 18:28:10 +00:00
|
|
|
public:
|
2015-01-23 02:34:23 +00:00
|
|
|
enum ExpectedVerifResult { kValExpectFound, kValExpectNoError };
|
|
|
|
enum ResetMethod {
|
|
|
|
kResetDropUnsyncedData,
|
2015-01-26 23:22:18 +00:00
|
|
|
kResetDropRandomUnsyncedData,
|
2015-01-23 02:34:23 +00:00
|
|
|
kResetDeleteUnsyncedFiles,
|
|
|
|
kResetDropAndDeleteUnsynced
|
|
|
|
};
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2015-01-27 22:44:19 +00:00
|
|
|
std::unique_ptr<Env> base_env_;
|
2015-01-15 18:28:10 +00:00
|
|
|
FaultInjectionTestEnv* env_;
|
|
|
|
std::string dbname_;
|
2018-11-09 19:17:34 +00:00
|
|
|
std::shared_ptr<Cache> tiny_cache_;
|
2015-01-15 18:28:10 +00:00
|
|
|
Options options_;
|
|
|
|
DB* db_;
|
|
|
|
|
2015-01-23 02:34:23 +00:00
|
|
|
FaultInjectionTest()
|
2018-05-07 19:15:54 +00:00
|
|
|
: option_config_(std::get<1>(GetParam())),
|
|
|
|
non_inclusive_end_range_(std::get<2>(GetParam())),
|
2015-01-24 00:03:24 +00:00
|
|
|
sync_use_wal_(false),
|
|
|
|
sync_use_compact_(true),
|
2015-01-27 22:44:19 +00:00
|
|
|
base_env_(nullptr),
|
2018-03-07 20:39:19 +00:00
|
|
|
env_(nullptr),
|
2018-05-07 19:15:54 +00:00
|
|
|
db_(nullptr) {}
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2019-02-14 21:52:47 +00:00
|
|
|
~FaultInjectionTest() override {
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
2015-07-16 19:18:32 +00:00
|
|
|
}
|
|
|
|
|
2015-01-23 02:34:23 +00:00
|
|
|
bool ChangeOptions() {
|
|
|
|
option_config_++;
|
2018-05-07 19:15:54 +00:00
|
|
|
if (option_config_ >= non_inclusive_end_range_) {
|
2015-01-23 02:34:23 +00:00
|
|
|
return false;
|
|
|
|
} else {
|
2015-01-27 22:44:19 +00:00
|
|
|
if (option_config_ == kMultiLevels) {
|
|
|
|
base_env_.reset(new MockEnv(Env::Default()));
|
|
|
|
}
|
2015-01-23 02:34:23 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the current option configuration.
|
|
|
|
Options CurrentOptions() {
|
2015-01-24 00:03:24 +00:00
|
|
|
sync_use_wal_ = false;
|
|
|
|
sync_use_compact_ = true;
|
2015-01-23 02:34:23 +00:00
|
|
|
Options options;
|
|
|
|
switch (option_config_) {
|
|
|
|
case kWalDir:
|
2018-07-14 00:18:39 +00:00
|
|
|
options.wal_dir = test::PerThreadDBPath(env_, "fault_test_wal");
|
2015-01-23 02:34:23 +00:00
|
|
|
break;
|
|
|
|
case kDifferentDataDir:
|
2018-07-14 00:18:39 +00:00
|
|
|
options.db_paths.emplace_back(
|
|
|
|
test::PerThreadDBPath(env_, "fault_test_data"), 1000000U);
|
2015-01-23 02:34:23 +00:00
|
|
|
break;
|
|
|
|
case kSyncWal:
|
2015-01-24 00:03:24 +00:00
|
|
|
sync_use_wal_ = true;
|
|
|
|
sync_use_compact_ = false;
|
2015-01-23 02:34:23 +00:00
|
|
|
break;
|
|
|
|
case kWalDirSyncWal:
|
2018-07-14 00:18:39 +00:00
|
|
|
options.wal_dir = test::PerThreadDBPath(env_, "/fault_test_wal");
|
2015-01-24 00:03:24 +00:00
|
|
|
sync_use_wal_ = true;
|
|
|
|
sync_use_compact_ = false;
|
2015-01-23 02:34:23 +00:00
|
|
|
break;
|
2015-01-27 22:44:19 +00:00
|
|
|
case kMultiLevels:
|
|
|
|
options.write_buffer_size = 64 * 1024;
|
|
|
|
options.target_file_size_base = 64 * 1024;
|
|
|
|
options.level0_file_num_compaction_trigger = 2;
|
|
|
|
options.level0_slowdown_writes_trigger = 2;
|
|
|
|
options.level0_stop_writes_trigger = 4;
|
|
|
|
options.max_bytes_for_level_base = 128 * 1024;
|
|
|
|
options.max_write_buffer_number = 2;
|
|
|
|
options.max_background_compactions = 8;
|
|
|
|
options.max_background_flushes = 8;
|
|
|
|
sync_use_wal_ = true;
|
|
|
|
sync_use_compact_ = false;
|
|
|
|
break;
|
2015-01-23 02:34:23 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
2015-01-15 18:28:10 +00:00
|
|
|
Status NewDB() {
|
2018-03-07 20:39:19 +00:00
|
|
|
assert(db_ == nullptr);
|
2015-01-15 18:28:10 +00:00
|
|
|
assert(tiny_cache_ == nullptr);
|
2018-03-07 20:39:19 +00:00
|
|
|
assert(env_ == nullptr);
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2015-01-27 22:44:19 +00:00
|
|
|
env_ =
|
|
|
|
new FaultInjectionTestEnv(base_env_ ? base_env_.get() : Env::Default());
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2015-01-23 02:34:23 +00:00
|
|
|
options_ = CurrentOptions();
|
2015-01-15 18:28:10 +00:00
|
|
|
options_.env = env_;
|
|
|
|
options_.paranoid_checks = true;
|
|
|
|
|
|
|
|
BlockBasedTableOptions table_options;
|
|
|
|
tiny_cache_ = NewLRUCache(100);
|
|
|
|
table_options.block_cache = tiny_cache_;
|
|
|
|
options_.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
|
|
|
|
2018-07-14 00:18:39 +00:00
|
|
|
dbname_ = test::PerThreadDBPath("fault_test");
|
2015-01-15 18:28:10 +00:00
|
|
|
|
rocksdb: Replace ASSERT* with EXPECT* in functions that does not return void value
Summary:
gtest does not use exceptions to fail a unit test by design, and `ASSERT*`s are implemented using `return`. As a consequence we cannot use `ASSERT*` in a function that does not return `void` value ([[ https://code.google.com/p/googletest/wiki/AdvancedGuide#Assertion_Placement | 1]]), and have to fix our existing code. This diff does this in a generic way, with no manual changes.
In order to detect all existing `ASSERT*` that are used in functions that doesn't return void value, I change the code to generate compile errors for such cases.
In `util/testharness.h` I defined `EXPECT*` assertions, the same way as `ASSERT*`, and redefined `ASSERT*` to return `void`. Then executed:
```lang=bash
% USE_CLANG=1 make all -j55 -k 2> build.log
% perl -naF: -e 'print "-- -number=".$F[1]." ".$F[0]."\n" if /: error:/' \
build.log | xargs -L 1 perl -spi -e 's/ASSERT/EXPECT/g if $. == $number'
% make format
```
After that I reverted back change to `ASSERT*` in `util/testharness.h`. But preserved introduced `EXPECT*`, which is the same as `ASSERT*`. This will be deleted once switched to gtest.
This diff is independent and contains manual changes only in `util/testharness.h`.
Test Plan:
Make sure all tests are passing.
```lang=bash
% USE_CLANG=1 make check
```
Reviewers: igor, lgalanis, sdong, yufei.zhu, rven, meyering
Reviewed By: meyering
Subscribers: dhruba, leveldb
Differential Revision: https://reviews.facebook.net/D33333
2015-03-17 03:52:32 +00:00
|
|
|
EXPECT_OK(DestroyDB(dbname_, options_));
|
2015-01-23 02:34:23 +00:00
|
|
|
|
2015-01-15 18:28:10 +00:00
|
|
|
options_.create_if_missing = true;
|
|
|
|
Status s = OpenDB();
|
|
|
|
options_.create_if_missing = false;
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
void SetUp() override {
|
2018-05-07 19:15:54 +00:00
|
|
|
sequential_order_ = std::get<0>(GetParam());
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(NewDB());
|
|
|
|
}
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2015-03-17 21:08:00 +00:00
|
|
|
void TearDown() override {
|
2015-01-15 18:28:10 +00:00
|
|
|
CloseDB();
|
|
|
|
|
2015-01-23 02:34:23 +00:00
|
|
|
Status s = DestroyDB(dbname_, options_);
|
2015-01-15 18:28:10 +00:00
|
|
|
|
|
|
|
delete env_;
|
2018-03-07 20:39:19 +00:00
|
|
|
env_ = nullptr;
|
2015-01-15 18:28:10 +00:00
|
|
|
|
|
|
|
tiny_cache_.reset();
|
|
|
|
|
2015-03-17 21:08:00 +00:00
|
|
|
ASSERT_OK(s);
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
void Build(const WriteOptions& write_options, int start_idx, int num_vals) {
|
2015-01-15 18:28:10 +00:00
|
|
|
std::string key_space, value_space;
|
|
|
|
WriteBatch batch;
|
|
|
|
for (int i = start_idx; i < start_idx + num_vals; i++) {
|
2015-07-16 19:18:32 +00:00
|
|
|
Slice key = Key(i, &key_space);
|
2015-01-15 18:28:10 +00:00
|
|
|
batch.Clear();
|
|
|
|
batch.Put(key, Value(i, &value_space));
|
2015-01-23 02:34:23 +00:00
|
|
|
ASSERT_OK(db_->Write(write_options, &batch));
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
Status ReadValue(int i, std::string* val) const {
|
2015-01-15 18:28:10 +00:00
|
|
|
std::string key_space, value_space;
|
2015-07-16 19:18:32 +00:00
|
|
|
Slice key = Key(i, &key_space);
|
2015-01-15 18:28:10 +00:00
|
|
|
Value(i, &value_space);
|
|
|
|
ReadOptions options;
|
|
|
|
return db_->Get(options, key, val);
|
|
|
|
}
|
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
Status Verify(int start_idx, int num_vals,
|
|
|
|
ExpectedVerifResult expected) const {
|
2015-01-15 18:28:10 +00:00
|
|
|
std::string val;
|
|
|
|
std::string value_space;
|
|
|
|
Status s;
|
|
|
|
for (int i = start_idx; i < start_idx + num_vals && s.ok(); i++) {
|
|
|
|
Value(i, &value_space);
|
2015-07-16 19:18:32 +00:00
|
|
|
s = ReadValue(i, &val);
|
2015-01-23 02:34:23 +00:00
|
|
|
if (s.ok()) {
|
rocksdb: Replace ASSERT* with EXPECT* in functions that does not return void value
Summary:
gtest does not use exceptions to fail a unit test by design, and `ASSERT*`s are implemented using `return`. As a consequence we cannot use `ASSERT*` in a function that does not return `void` value ([[ https://code.google.com/p/googletest/wiki/AdvancedGuide#Assertion_Placement | 1]]), and have to fix our existing code. This diff does this in a generic way, with no manual changes.
In order to detect all existing `ASSERT*` that are used in functions that doesn't return void value, I change the code to generate compile errors for such cases.
In `util/testharness.h` I defined `EXPECT*` assertions, the same way as `ASSERT*`, and redefined `ASSERT*` to return `void`. Then executed:
```lang=bash
% USE_CLANG=1 make all -j55 -k 2> build.log
% perl -naF: -e 'print "-- -number=".$F[1]." ".$F[0]."\n" if /: error:/' \
build.log | xargs -L 1 perl -spi -e 's/ASSERT/EXPECT/g if $. == $number'
% make format
```
After that I reverted back change to `ASSERT*` in `util/testharness.h`. But preserved introduced `EXPECT*`, which is the same as `ASSERT*`. This will be deleted once switched to gtest.
This diff is independent and contains manual changes only in `util/testharness.h`.
Test Plan:
Make sure all tests are passing.
```lang=bash
% USE_CLANG=1 make check
```
Reviewers: igor, lgalanis, sdong, yufei.zhu, rven, meyering
Reviewed By: meyering
Subscribers: dhruba, leveldb
Differential Revision: https://reviews.facebook.net/D33333
2015-03-17 03:52:32 +00:00
|
|
|
EXPECT_EQ(value_space, val);
|
2015-01-23 02:34:23 +00:00
|
|
|
}
|
|
|
|
if (expected == kValExpectFound) {
|
|
|
|
if (!s.ok()) {
|
|
|
|
fprintf(stderr, "Error when read %dth record (expect found): %s\n", i,
|
|
|
|
s.ToString().c_str());
|
|
|
|
return s;
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
2015-01-23 02:34:23 +00:00
|
|
|
} else if (!s.ok() && !s.IsNotFound()) {
|
|
|
|
fprintf(stderr, "Error when read %dth record: %s\n", i,
|
|
|
|
s.ToString().c_str());
|
|
|
|
return s;
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
}
|
2015-01-23 02:34:23 +00:00
|
|
|
return Status::OK();
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return the ith key
|
2015-07-16 19:18:32 +00:00
|
|
|
Slice Key(int i, std::string* storage) const {
|
2018-02-13 22:07:48 +00:00
|
|
|
unsigned long long num = i;
|
2015-07-16 19:18:32 +00:00
|
|
|
if (!sequential_order_) {
|
2015-07-16 02:58:28 +00:00
|
|
|
// random transfer
|
|
|
|
const int m = 0x5bd1e995;
|
|
|
|
num *= m;
|
|
|
|
num ^= num << 24;
|
|
|
|
}
|
2015-01-15 18:28:10 +00:00
|
|
|
char buf[100];
|
2018-02-13 22:07:48 +00:00
|
|
|
snprintf(buf, sizeof(buf), "%016d", static_cast<int>(num));
|
2015-01-15 18:28:10 +00:00
|
|
|
storage->assign(buf, strlen(buf));
|
|
|
|
return Slice(*storage);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the value to associate with the specified key
|
|
|
|
Slice Value(int k, std::string* storage) const {
|
|
|
|
Random r(k);
|
|
|
|
return test::RandomString(&r, kValueSize, storage);
|
|
|
|
}
|
|
|
|
|
2015-12-15 23:26:20 +00:00
|
|
|
void CloseDB() {
|
2015-01-15 18:28:10 +00:00
|
|
|
delete db_;
|
2017-05-19 17:43:11 +00:00
|
|
|
db_ = nullptr;
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
2015-12-15 23:26:20 +00:00
|
|
|
Status OpenDB() {
|
|
|
|
CloseDB();
|
|
|
|
env_->ResetState();
|
2017-05-19 17:43:11 +00:00
|
|
|
Status s = DB::Open(options_, dbname_, &db_);
|
|
|
|
assert(db_ != nullptr);
|
|
|
|
return s;
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void DeleteAllData() {
|
|
|
|
Iterator* iter = db_->NewIterator(ReadOptions());
|
|
|
|
WriteOptions options;
|
|
|
|
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
|
|
|
ASSERT_OK(db_->Delete(WriteOptions(), iter->key()));
|
|
|
|
}
|
|
|
|
|
|
|
|
delete iter;
|
2015-01-23 02:34:23 +00:00
|
|
|
|
|
|
|
FlushOptions flush_options;
|
|
|
|
flush_options.wait = true;
|
|
|
|
db_->Flush(flush_options);
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
2015-01-26 23:22:18 +00:00
|
|
|
// rnd cannot be null for kResetDropRandomUnsyncedData
|
|
|
|
void ResetDBState(ResetMethod reset_method, Random* rnd = nullptr) {
|
2015-01-28 00:34:16 +00:00
|
|
|
env_->AssertNoOpenFile();
|
2015-01-15 18:28:10 +00:00
|
|
|
switch (reset_method) {
|
2015-01-23 02:34:23 +00:00
|
|
|
case kResetDropUnsyncedData:
|
2015-01-15 18:28:10 +00:00
|
|
|
ASSERT_OK(env_->DropUnsyncedFileData());
|
|
|
|
break;
|
2015-01-26 23:22:18 +00:00
|
|
|
case kResetDropRandomUnsyncedData:
|
|
|
|
ASSERT_OK(env_->DropRandomUnsyncedFileData(rnd));
|
|
|
|
break;
|
2015-01-23 02:34:23 +00:00
|
|
|
case kResetDeleteUnsyncedFiles:
|
|
|
|
ASSERT_OK(env_->DeleteFilesCreatedAfterLastDirSync());
|
|
|
|
break;
|
|
|
|
case kResetDropAndDeleteUnsynced:
|
|
|
|
ASSERT_OK(env_->DropUnsyncedFileData());
|
2015-01-15 18:28:10 +00:00
|
|
|
ASSERT_OK(env_->DeleteFilesCreatedAfterLastDirSync());
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
assert(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void PartialCompactTestPreFault(int num_pre_sync, int num_post_sync) {
|
|
|
|
DeleteAllData();
|
2015-01-23 02:34:23 +00:00
|
|
|
|
|
|
|
WriteOptions write_options;
|
2015-01-24 00:03:24 +00:00
|
|
|
write_options.sync = sync_use_wal_;
|
2015-01-23 02:34:23 +00:00
|
|
|
|
|
|
|
Build(write_options, 0, num_pre_sync);
|
2015-01-24 00:03:24 +00:00
|
|
|
if (sync_use_compact_) {
|
2015-06-17 21:36:14 +00:00
|
|
|
db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
|
2015-01-23 02:34:23 +00:00
|
|
|
}
|
|
|
|
write_options.sync = false;
|
|
|
|
Build(write_options, num_pre_sync, num_post_sync);
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void PartialCompactTestReopenWithFault(ResetMethod reset_method,
|
2015-01-26 23:22:18 +00:00
|
|
|
int num_pre_sync, int num_post_sync,
|
|
|
|
Random* rnd = nullptr) {
|
2015-01-15 18:28:10 +00:00
|
|
|
env_->SetFilesystemActive(false);
|
|
|
|
CloseDB();
|
2015-01-26 23:22:18 +00:00
|
|
|
ResetDBState(reset_method, rnd);
|
2015-01-15 18:28:10 +00:00
|
|
|
ASSERT_OK(OpenDB());
|
2015-01-23 02:34:23 +00:00
|
|
|
ASSERT_OK(Verify(0, num_pre_sync, FaultInjectionTest::kValExpectFound));
|
2015-01-15 18:28:10 +00:00
|
|
|
ASSERT_OK(Verify(num_pre_sync, num_post_sync,
|
2015-01-23 02:34:23 +00:00
|
|
|
FaultInjectionTest::kValExpectNoError));
|
2015-07-16 19:18:32 +00:00
|
|
|
WaitCompactionFinish();
|
|
|
|
ASSERT_OK(Verify(0, num_pre_sync, FaultInjectionTest::kValExpectFound));
|
|
|
|
ASSERT_OK(Verify(num_pre_sync, num_post_sync,
|
|
|
|
FaultInjectionTest::kValExpectNoError));
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NoWriteTestPreFault() {
|
|
|
|
}
|
|
|
|
|
|
|
|
void NoWriteTestReopenWithFault(ResetMethod reset_method) {
|
|
|
|
CloseDB();
|
|
|
|
ResetDBState(reset_method);
|
|
|
|
ASSERT_OK(OpenDB());
|
|
|
|
}
|
2015-07-16 19:18:32 +00:00
|
|
|
|
|
|
|
void WaitCompactionFinish() {
|
[rocksdb] Recovery path sequence miscount fix
Summary:
Consider the following WAL with 4 batch entries prefixed with their sequence at time of memtable insert.
[1: BEGIN_PREPARE, PUT, PUT, PUT, PUT, END_PREPARE(a)]
[1: BEGIN_PREPARE, PUT, PUT, PUT, PUT, END_PREPARE(b)]
[4: COMMIT(a)]
[7: COMMIT(b)]
The first two batches do not consume any sequence numbers so are both prefixed with seq=1.
For 2pc commit, memtable insertion takes place before COMMIT batch is written to WAL.
We can see that sequence number consumption takes place between WAL entries giving us the seemingly sparse sequence prefix for WAL entries.
This is a valid WAL.
Because with 2PC markers one WriteBatch points to another batch containing its inserts a writebatch can consume more or less sequence numbers than the number of sequence consuming entries that it contains.
We can see that, given the entries in the WAL, 6 sequence ids were consumed. Yet on recovery the maximum sequence consumed would be 7 + 3 (the number of sequence numbers consumed by COMMIT(b))
So, now upon recovery we must track the actual consumption of sequence numbers.
In the provided scenario there will be no sequence gaps, but it is possible to produce a sequence gap. This should not be a problem though. correct?
Test Plan: provided test.
Reviewers: sdong
Subscribers: andrewkr, leveldb, dhruba, hermanlee4
Differential Revision: https://reviews.facebook.net/D57645
2016-05-04 21:02:27 +00:00
|
|
|
static_cast<DBImpl*>(db_->GetRootDB())->TEST_WaitForCompact();
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(db_->Put(WriteOptions(), "", ""));
|
|
|
|
}
|
2015-01-15 18:28:10 +00:00
|
|
|
};
|
|
|
|
|
2018-05-07 19:15:54 +00:00
|
|
|
class FaultInjectionTestSplitted : public FaultInjectionTest {};
|
|
|
|
|
|
|
|
TEST_P(FaultInjectionTestSplitted, FaultTest) {
|
2015-01-23 02:34:23 +00:00
|
|
|
do {
|
|
|
|
Random rnd(301);
|
|
|
|
|
2015-01-24 00:26:38 +00:00
|
|
|
for (size_t idx = 0; idx < kNumIterations; idx++) {
|
|
|
|
int num_pre_sync = rnd.Uniform(kMaxNumValues);
|
|
|
|
int num_post_sync = rnd.Uniform(kMaxNumValues);
|
2015-01-15 18:28:10 +00:00
|
|
|
|
2015-01-23 02:34:23 +00:00
|
|
|
PartialCompactTestPreFault(num_pre_sync, num_post_sync);
|
2015-01-24 00:26:38 +00:00
|
|
|
PartialCompactTestReopenWithFault(kResetDropUnsyncedData, num_pre_sync,
|
|
|
|
num_post_sync);
|
2015-01-23 02:34:23 +00:00
|
|
|
NoWriteTestPreFault();
|
2015-01-24 00:26:38 +00:00
|
|
|
NoWriteTestReopenWithFault(kResetDropUnsyncedData);
|
|
|
|
|
2015-01-26 23:22:18 +00:00
|
|
|
PartialCompactTestPreFault(num_pre_sync, num_post_sync);
|
|
|
|
PartialCompactTestReopenWithFault(kResetDropRandomUnsyncedData,
|
|
|
|
num_pre_sync, num_post_sync, &rnd);
|
|
|
|
NoWriteTestPreFault();
|
|
|
|
NoWriteTestReopenWithFault(kResetDropUnsyncedData);
|
|
|
|
|
2015-01-24 00:26:38 +00:00
|
|
|
// Setting a separate data path won't pass the test as we don't sync
|
|
|
|
// it after creating new files,
|
2015-01-26 21:59:38 +00:00
|
|
|
PartialCompactTestPreFault(num_pre_sync, num_post_sync);
|
|
|
|
PartialCompactTestReopenWithFault(kResetDropAndDeleteUnsynced,
|
|
|
|
num_pre_sync, num_post_sync);
|
|
|
|
NoWriteTestPreFault();
|
|
|
|
NoWriteTestReopenWithFault(kResetDropAndDeleteUnsynced);
|
|
|
|
|
|
|
|
PartialCompactTestPreFault(num_pre_sync, num_post_sync);
|
|
|
|
// No new files created so we expect all values since no files will be
|
|
|
|
// dropped.
|
|
|
|
PartialCompactTestReopenWithFault(kResetDeleteUnsyncedFiles, num_pre_sync,
|
|
|
|
num_post_sync);
|
|
|
|
NoWriteTestPreFault();
|
|
|
|
NoWriteTestReopenWithFault(kResetDeleteUnsyncedFiles);
|
2015-01-23 02:34:23 +00:00
|
|
|
}
|
|
|
|
} while (ChangeOptions());
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 04:18:05 +00:00
|
|
|
// Previous log file is not fsynced if sync is forced after log rolling.
|
[wal changes 2/3] write with sync=true syncs previous unsynced wals to prevent illegal data loss
Summary:
I'll just copy internal task summary here:
"
This sequence will cause data loss in the middle after an sync write:
non-sync write key 1
flush triggered, not yet scheduled
sync write key 2
system crash
After rebooting, users might see key 2 but not key 1, which violates the API of sync write.
This can be reproduced using unit test FaultInjectionTest::DISABLED_WriteOptionSyncTest.
One way to fix it is for a sync write, if there is outstanding unsynced log files, we need to syc them too.
"
This diff should be considered together with the next diff D40905; in isolation this fix probably could be a little simpler.
Test Plan: `make check`; added a test for that (DBTest.SyncingPreviousLogs) before noticing FaultInjectionTest.WriteOptionSyncTest (keeping both since mine asserts a bit more); both tests fail without this diff; for D40905 stacked on top of this diff, ran tests with ASAN, TSAN and valgrind
Reviewers: rven, yhchiang, IslamAbdelRahman, anthony, kradhakrishnan, igor, sdong
Reviewed By: sdong
Subscribers: dhruba
Differential Revision: https://reviews.facebook.net/D40899
2015-07-22 10:28:08 +00:00
|
|
|
TEST_P(FaultInjectionTest, WriteOptionSyncTest) {
|
2015-09-25 17:29:44 +00:00
|
|
|
test::SleepingBackgroundTask sleeping_task_low;
|
2015-04-09 04:18:05 +00:00
|
|
|
env_->SetBackgroundThreads(1, Env::HIGH);
|
|
|
|
// Block the job queue to prevent flush job from running.
|
2015-09-25 17:29:44 +00:00
|
|
|
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task_low,
|
2015-04-09 04:18:05 +00:00
|
|
|
Env::Priority::HIGH);
|
2016-01-06 01:56:29 +00:00
|
|
|
sleeping_task_low.WaitUntilSleeping();
|
2015-04-09 04:18:05 +00:00
|
|
|
|
|
|
|
WriteOptions write_options;
|
|
|
|
write_options.sync = false;
|
|
|
|
|
|
|
|
std::string key_space, value_space;
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(
|
|
|
|
db_->Put(write_options, Key(1, &key_space), Value(1, &value_space)));
|
2015-04-09 04:18:05 +00:00
|
|
|
FlushOptions flush_options;
|
|
|
|
flush_options.wait = false;
|
|
|
|
ASSERT_OK(db_->Flush(flush_options));
|
|
|
|
write_options.sync = true;
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(
|
|
|
|
db_->Put(write_options, Key(2, &key_space), Value(2, &value_space)));
|
2017-06-24 21:06:43 +00:00
|
|
|
db_->FlushWAL(false);
|
2015-04-09 04:18:05 +00:00
|
|
|
|
|
|
|
env_->SetFilesystemActive(false);
|
|
|
|
NoWriteTestReopenWithFault(kResetDropAndDeleteUnsynced);
|
|
|
|
sleeping_task_low.WakeUp();
|
2016-08-10 17:58:15 +00:00
|
|
|
sleeping_task_low.WaitUntilDone();
|
2015-04-09 04:18:05 +00:00
|
|
|
|
|
|
|
ASSERT_OK(OpenDB());
|
|
|
|
std::string val;
|
|
|
|
Value(2, &value_space);
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(ReadValue(2, &val));
|
2015-04-09 04:18:05 +00:00
|
|
|
ASSERT_EQ(value_space, val);
|
|
|
|
|
|
|
|
Value(1, &value_space);
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(ReadValue(1, &val));
|
2015-04-09 04:18:05 +00:00
|
|
|
ASSERT_EQ(value_space, val);
|
|
|
|
}
|
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
TEST_P(FaultInjectionTest, UninstalledCompaction) {
|
2015-07-16 02:58:28 +00:00
|
|
|
options_.target_file_size_base = 32 * 1024;
|
|
|
|
options_.write_buffer_size = 100 << 10; // 100KB
|
|
|
|
options_.level0_file_num_compaction_trigger = 6;
|
|
|
|
options_.level0_stop_writes_trigger = 1 << 10;
|
|
|
|
options_.level0_slowdown_writes_trigger = 1 << 10;
|
|
|
|
options_.max_background_compactions = 1;
|
|
|
|
OpenDB();
|
|
|
|
|
2015-07-16 19:18:32 +00:00
|
|
|
if (!sequential_order_) {
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
2015-07-16 19:18:32 +00:00
|
|
|
{"FaultInjectionTest::FaultTest:0", "DBImpl::BGWorkCompaction"},
|
|
|
|
{"CompactionJob::Run():End", "FaultInjectionTest::FaultTest:1"},
|
|
|
|
{"FaultInjectionTest::FaultTest:2",
|
|
|
|
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun"},
|
|
|
|
});
|
|
|
|
}
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
2015-07-16 02:58:28 +00:00
|
|
|
|
|
|
|
int kNumKeys = 1000;
|
2015-07-16 19:18:32 +00:00
|
|
|
Build(WriteOptions(), 0, kNumKeys);
|
2015-07-16 02:58:28 +00:00
|
|
|
FlushOptions flush_options;
|
|
|
|
flush_options.wait = true;
|
|
|
|
db_->Flush(flush_options);
|
|
|
|
ASSERT_OK(db_->Put(WriteOptions(), "", ""));
|
|
|
|
TEST_SYNC_POINT("FaultInjectionTest::FaultTest:0");
|
|
|
|
TEST_SYNC_POINT("FaultInjectionTest::FaultTest:1");
|
|
|
|
env_->SetFilesystemActive(false);
|
|
|
|
TEST_SYNC_POINT("FaultInjectionTest::FaultTest:2");
|
|
|
|
CloseDB();
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
2015-07-16 02:58:28 +00:00
|
|
|
ResetDBState(kResetDropUnsyncedData);
|
|
|
|
|
|
|
|
std::atomic<bool> opened(false);
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
2018-04-13 00:55:14 +00:00
|
|
|
"DBImpl::Open:Opened", [&](void* /*arg*/) { opened.store(true); });
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
2015-07-16 02:58:28 +00:00
|
|
|
"DBImpl::BGWorkCompaction",
|
2018-04-13 00:55:14 +00:00
|
|
|
[&](void* /*arg*/) { ASSERT_TRUE(opened.load()); });
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
2015-07-16 02:58:28 +00:00
|
|
|
ASSERT_OK(OpenDB());
|
2015-07-16 19:18:32 +00:00
|
|
|
ASSERT_OK(Verify(0, kNumKeys, FaultInjectionTest::kValExpectFound));
|
|
|
|
WaitCompactionFinish();
|
|
|
|
ASSERT_OK(Verify(0, kNumKeys, FaultInjectionTest::kValExpectFound));
|
2020-02-20 20:07:53 +00:00
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
2015-07-16 02:58:28 +00:00
|
|
|
}
|
|
|
|
|
[wal changes 3/3] method in DB to sync WAL without blocking writers
Summary:
Subj. We really need this feature.
Previous diff D40899 has most of the changes to make this possible, this diff just adds the method.
Test Plan: `make check`, the new test fails without this diff; ran with ASAN, TSAN and valgrind.
Reviewers: igor, rven, IslamAbdelRahman, anthony, kradhakrishnan, tnovak, yhchiang, sdong
Reviewed By: sdong
Subscribers: MarkCallaghan, maykov, hermanlee4, yoshinorim, tnovak, dhruba
Differential Revision: https://reviews.facebook.net/D40905
2015-08-05 13:06:39 +00:00
|
|
|
TEST_P(FaultInjectionTest, ManualLogSyncTest) {
|
2015-09-25 17:29:44 +00:00
|
|
|
test::SleepingBackgroundTask sleeping_task_low;
|
[wal changes 3/3] method in DB to sync WAL without blocking writers
Summary:
Subj. We really need this feature.
Previous diff D40899 has most of the changes to make this possible, this diff just adds the method.
Test Plan: `make check`, the new test fails without this diff; ran with ASAN, TSAN and valgrind.
Reviewers: igor, rven, IslamAbdelRahman, anthony, kradhakrishnan, tnovak, yhchiang, sdong
Reviewed By: sdong
Subscribers: MarkCallaghan, maykov, hermanlee4, yoshinorim, tnovak, dhruba
Differential Revision: https://reviews.facebook.net/D40905
2015-08-05 13:06:39 +00:00
|
|
|
env_->SetBackgroundThreads(1, Env::HIGH);
|
|
|
|
// Block the job queue to prevent flush job from running.
|
2015-09-25 17:29:44 +00:00
|
|
|
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task_low,
|
[wal changes 3/3] method in DB to sync WAL without blocking writers
Summary:
Subj. We really need this feature.
Previous diff D40899 has most of the changes to make this possible, this diff just adds the method.
Test Plan: `make check`, the new test fails without this diff; ran with ASAN, TSAN and valgrind.
Reviewers: igor, rven, IslamAbdelRahman, anthony, kradhakrishnan, tnovak, yhchiang, sdong
Reviewed By: sdong
Subscribers: MarkCallaghan, maykov, hermanlee4, yoshinorim, tnovak, dhruba
Differential Revision: https://reviews.facebook.net/D40905
2015-08-05 13:06:39 +00:00
|
|
|
Env::Priority::HIGH);
|
2016-01-06 01:56:29 +00:00
|
|
|
sleeping_task_low.WaitUntilSleeping();
|
[wal changes 3/3] method in DB to sync WAL without blocking writers
Summary:
Subj. We really need this feature.
Previous diff D40899 has most of the changes to make this possible, this diff just adds the method.
Test Plan: `make check`, the new test fails without this diff; ran with ASAN, TSAN and valgrind.
Reviewers: igor, rven, IslamAbdelRahman, anthony, kradhakrishnan, tnovak, yhchiang, sdong
Reviewed By: sdong
Subscribers: MarkCallaghan, maykov, hermanlee4, yoshinorim, tnovak, dhruba
Differential Revision: https://reviews.facebook.net/D40905
2015-08-05 13:06:39 +00:00
|
|
|
|
|
|
|
WriteOptions write_options;
|
|
|
|
write_options.sync = false;
|
|
|
|
|
|
|
|
std::string key_space, value_space;
|
|
|
|
ASSERT_OK(
|
|
|
|
db_->Put(write_options, Key(1, &key_space), Value(1, &value_space)));
|
|
|
|
FlushOptions flush_options;
|
|
|
|
flush_options.wait = false;
|
|
|
|
ASSERT_OK(db_->Flush(flush_options));
|
|
|
|
ASSERT_OK(
|
|
|
|
db_->Put(write_options, Key(2, &key_space), Value(2, &value_space)));
|
2017-06-24 21:06:43 +00:00
|
|
|
ASSERT_OK(db_->FlushWAL(true));
|
[wal changes 3/3] method in DB to sync WAL without blocking writers
Summary:
Subj. We really need this feature.
Previous diff D40899 has most of the changes to make this possible, this diff just adds the method.
Test Plan: `make check`, the new test fails without this diff; ran with ASAN, TSAN and valgrind.
Reviewers: igor, rven, IslamAbdelRahman, anthony, kradhakrishnan, tnovak, yhchiang, sdong
Reviewed By: sdong
Subscribers: MarkCallaghan, maykov, hermanlee4, yoshinorim, tnovak, dhruba
Differential Revision: https://reviews.facebook.net/D40905
2015-08-05 13:06:39 +00:00
|
|
|
|
|
|
|
env_->SetFilesystemActive(false);
|
|
|
|
NoWriteTestReopenWithFault(kResetDropAndDeleteUnsynced);
|
|
|
|
sleeping_task_low.WakeUp();
|
2016-08-10 17:58:15 +00:00
|
|
|
sleeping_task_low.WaitUntilDone();
|
[wal changes 3/3] method in DB to sync WAL without blocking writers
Summary:
Subj. We really need this feature.
Previous diff D40899 has most of the changes to make this possible, this diff just adds the method.
Test Plan: `make check`, the new test fails without this diff; ran with ASAN, TSAN and valgrind.
Reviewers: igor, rven, IslamAbdelRahman, anthony, kradhakrishnan, tnovak, yhchiang, sdong
Reviewed By: sdong
Subscribers: MarkCallaghan, maykov, hermanlee4, yoshinorim, tnovak, dhruba
Differential Revision: https://reviews.facebook.net/D40905
2015-08-05 13:06:39 +00:00
|
|
|
|
|
|
|
ASSERT_OK(OpenDB());
|
|
|
|
std::string val;
|
|
|
|
Value(2, &value_space);
|
|
|
|
ASSERT_OK(ReadValue(2, &val));
|
|
|
|
ASSERT_EQ(value_space, val);
|
|
|
|
|
|
|
|
Value(1, &value_space);
|
|
|
|
ASSERT_OK(ReadValue(1, &val));
|
|
|
|
ASSERT_EQ(value_space, val);
|
|
|
|
}
|
|
|
|
|
Add facility to write only a portion of WriteBatch to WAL
Summary:
When constructing a write batch a client may now call MarkWalTerminationPoint() on that batch. No batch operations after this call will be added written to the WAL but will still be inserted into the Memtable. This facility is used to remove one of the three WriteImpl calls in 2PC transactions. This produces a ~1% perf improvement.
```
RocksDB - unoptimized 2pc, sync_binlog=1, disable_2pc=off
INFO 2016-08-31 14:30:38,814 [main]: REQUEST PHASE COMPLETED. 75000000 requests done in 2619 seconds. Requests/second = 28628
RocksDB - optimized 2pc , sync_binlog=1, disable_2pc=off
INFO 2016-08-31 16:26:59,442 [main]: REQUEST PHASE COMPLETED. 75000000 requests done in 2581 seconds. Requests/second = 29054
```
Test Plan: Two unit tests added.
Reviewers: sdong, yiwu, IslamAbdelRahman
Reviewed By: yiwu
Subscribers: hermanlee4, dhruba, andrewkr
Differential Revision: https://reviews.facebook.net/D64599
2016-10-07 18:31:26 +00:00
|
|
|
TEST_P(FaultInjectionTest, WriteBatchWalTerminationTest) {
|
|
|
|
ReadOptions ro;
|
|
|
|
Options options = CurrentOptions();
|
|
|
|
options.env = env_;
|
|
|
|
|
|
|
|
WriteOptions wo;
|
|
|
|
wo.sync = true;
|
|
|
|
wo.disableWAL = false;
|
|
|
|
WriteBatch batch;
|
|
|
|
batch.Put("cats", "dogs");
|
|
|
|
batch.MarkWalTerminationPoint();
|
|
|
|
batch.Put("boys", "girls");
|
|
|
|
ASSERT_OK(db_->Write(wo, &batch));
|
|
|
|
|
|
|
|
env_->SetFilesystemActive(false);
|
|
|
|
NoWriteTestReopenWithFault(kResetDropAndDeleteUnsynced);
|
|
|
|
ASSERT_OK(OpenDB());
|
|
|
|
|
|
|
|
std::string val;
|
|
|
|
ASSERT_OK(db_->Get(ro, "cats", &val));
|
|
|
|
ASSERT_EQ("dogs", val);
|
|
|
|
ASSERT_EQ(db_->Get(ro, "boys", &val), Status::NotFound());
|
|
|
|
}
|
|
|
|
|
2020-06-03 22:53:09 +00:00
|
|
|
INSTANTIATE_TEST_CASE_P(
|
2018-05-07 19:15:54 +00:00
|
|
|
FaultTest, FaultInjectionTest,
|
|
|
|
::testing::Values(std::make_tuple(false, kDefault, kEnd),
|
|
|
|
std::make_tuple(true, kDefault, kEnd)));
|
|
|
|
|
2020-06-03 22:53:09 +00:00
|
|
|
INSTANTIATE_TEST_CASE_P(
|
2018-05-07 19:15:54 +00:00
|
|
|
FaultTest, FaultInjectionTestSplitted,
|
|
|
|
::testing::Values(std::make_tuple(false, kDefault, kSyncWal),
|
|
|
|
std::make_tuple(true, kDefault, kSyncWal),
|
|
|
|
std::make_tuple(false, kSyncWal, kEnd),
|
|
|
|
std::make_tuple(true, kSyncWal, kEnd)));
|
2015-07-16 19:18:32 +00:00
|
|
|
|
2020-02-20 20:07:53 +00:00
|
|
|
} // namespace ROCKSDB_NAMESPACE
|
2015-01-15 18:28:10 +00:00
|
|
|
|
|
|
|
int main(int argc, char** argv) {
|
2015-03-17 21:08:00 +00:00
|
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
|
|
return RUN_ALL_TESTS();
|
2015-01-15 18:28:10 +00:00
|
|
|
}
|