18dcf7f98d
Summary: Add PreReleaseCallback to be called at the end of WriteImpl but before publishing the sequence number. The callback is used in WritePrepareTxn to i) update the commit map, ii) update the last published sequence number in the 2nd write queue. It also ensures that all the commits will go to the 2nd queue. These changes will ensure that the commit map is updated before the sequence number is published and used by reading snapshots. If we use two write queues, the snapshots will use the seq number published by the 2nd queue. If we use one write queue (the default, the snapshots will use the last seq number in the memtable, which also indicates the last published seq number. Closes https://github.com/facebook/rocksdb/pull/3205 Differential Revision: D6438959 Pulled By: maysamyabandeh fbshipit-source-id: f8b6c434e94bc5f5ab9cb696879d4c23e2577ab9
390 lines
12 KiB
C++
390 lines
12 KiB
C++
// 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).
|
|
|
|
#pragma once
|
|
|
|
#ifndef __STDC_FORMAT_MACROS
|
|
#define __STDC_FORMAT_MACROS
|
|
#endif
|
|
|
|
#include <inttypes.h>
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
#include "db/db_impl.h"
|
|
#include "rocksdb/db.h"
|
|
#include "rocksdb/options.h"
|
|
#include "rocksdb/utilities/transaction.h"
|
|
#include "rocksdb/utilities/transaction_db.h"
|
|
#include "table/mock_table.h"
|
|
#include "util/fault_injection_test_env.h"
|
|
#include "util/random.h"
|
|
#include "util/string_util.h"
|
|
#include "util/sync_point.h"
|
|
#include "util/testharness.h"
|
|
#include "util/testutil.h"
|
|
#include "util/transaction_test_util.h"
|
|
#include "utilities/merge_operators.h"
|
|
#include "utilities/merge_operators/string_append/stringappend.h"
|
|
#include "utilities/transactions/pessimistic_transaction_db.h"
|
|
|
|
#include "port/port.h"
|
|
|
|
namespace rocksdb {
|
|
|
|
class TransactionTest : public ::testing::TestWithParam<
|
|
std::tuple<bool, bool, TxnDBWritePolicy>> {
|
|
public:
|
|
TransactionDB* db;
|
|
FaultInjectionTestEnv* env;
|
|
std::string dbname;
|
|
Options options;
|
|
|
|
TransactionDBOptions txn_db_options;
|
|
|
|
TransactionTest() {
|
|
options.create_if_missing = true;
|
|
options.max_write_buffer_number = 2;
|
|
options.write_buffer_size = 4 * 1024;
|
|
options.level0_file_num_compaction_trigger = 2;
|
|
options.merge_operator = MergeOperators::CreateFromStringId("stringappend");
|
|
env = new FaultInjectionTestEnv(Env::Default());
|
|
options.env = env;
|
|
options.two_write_queues = std::get<1>(GetParam());
|
|
dbname = test::TmpDir() + "/transaction_testdb";
|
|
|
|
DestroyDB(dbname, options);
|
|
txn_db_options.transaction_lock_timeout = 0;
|
|
txn_db_options.default_lock_timeout = 0;
|
|
txn_db_options.write_policy = std::get<2>(GetParam());
|
|
Status s;
|
|
if (std::get<0>(GetParam()) == false) {
|
|
s = TransactionDB::Open(options, txn_db_options, dbname, &db);
|
|
} else {
|
|
s = OpenWithStackableDB();
|
|
}
|
|
assert(s.ok());
|
|
}
|
|
|
|
~TransactionTest() {
|
|
delete db;
|
|
DestroyDB(dbname, options);
|
|
delete env;
|
|
}
|
|
|
|
Status ReOpenNoDelete() {
|
|
delete db;
|
|
db = nullptr;
|
|
env->AssertNoOpenFile();
|
|
env->DropUnsyncedFileData();
|
|
env->ResetState();
|
|
Status s;
|
|
if (std::get<0>(GetParam()) == false) {
|
|
s = TransactionDB::Open(options, txn_db_options, dbname, &db);
|
|
} else {
|
|
s = OpenWithStackableDB();
|
|
}
|
|
return s;
|
|
}
|
|
|
|
Status ReOpen() {
|
|
delete db;
|
|
DestroyDB(dbname, options);
|
|
Status s;
|
|
if (std::get<0>(GetParam()) == false) {
|
|
s = TransactionDB::Open(options, txn_db_options, dbname, &db);
|
|
} else {
|
|
s = OpenWithStackableDB();
|
|
}
|
|
return s;
|
|
}
|
|
|
|
Status OpenWithStackableDB() {
|
|
std::vector<size_t> compaction_enabled_cf_indices;
|
|
std::vector<ColumnFamilyDescriptor> column_families{ColumnFamilyDescriptor(
|
|
kDefaultColumnFamilyName, ColumnFamilyOptions(options))};
|
|
|
|
TransactionDB::PrepareWrap(&options, &column_families,
|
|
&compaction_enabled_cf_indices);
|
|
std::vector<ColumnFamilyHandle*> handles;
|
|
DB* root_db;
|
|
Options options_copy(options);
|
|
const bool use_seq_per_batch =
|
|
txn_db_options.write_policy == WRITE_PREPARED;
|
|
Status s = DBImpl::Open(options_copy, dbname, column_families, &handles,
|
|
&root_db, use_seq_per_batch);
|
|
if (s.ok()) {
|
|
assert(handles.size() == 1);
|
|
s = TransactionDB::WrapStackableDB(
|
|
new StackableDB(root_db), txn_db_options,
|
|
compaction_enabled_cf_indices, handles, &db);
|
|
delete handles[0];
|
|
}
|
|
return s;
|
|
}
|
|
|
|
std::atomic<size_t> linked = {0};
|
|
std::atomic<size_t> exp_seq = {0};
|
|
std::atomic<size_t> commit_writes = {0};
|
|
std::atomic<size_t> expected_commits = {0};
|
|
std::function<void(size_t, Status)> txn_t0_with_status = [&](size_t index,
|
|
Status exp_s) {
|
|
// Test DB's internal txn. It involves no prepare phase nor a commit marker.
|
|
WriteOptions wopts;
|
|
auto s = db->Put(wopts, "key" + std::to_string(index), "value");
|
|
ASSERT_EQ(exp_s, s);
|
|
if (txn_db_options.write_policy == TxnDBWritePolicy::WRITE_COMMITTED) {
|
|
// Consume one seq per key
|
|
exp_seq++;
|
|
} else {
|
|
// Consume one seq per batch
|
|
exp_seq++;
|
|
// Consume one seq for commit
|
|
exp_seq++;
|
|
}
|
|
};
|
|
std::function<void(size_t)> txn_t0 = [&](size_t index) {
|
|
return txn_t0_with_status(index, Status::OK());
|
|
};
|
|
std::function<void(size_t)> txn_t1 = [&](size_t index) {
|
|
// Testing directly writing a write batch. Functionality-wise it is
|
|
// equivalent to commit without prepare.
|
|
WriteBatch wb;
|
|
auto istr = std::to_string(index);
|
|
wb.Put("k1" + istr, "v1");
|
|
wb.Put("k2" + istr, "v2");
|
|
wb.Put("k3" + istr, "v3");
|
|
WriteOptions wopts;
|
|
auto s = db->Write(wopts, &wb);
|
|
if (txn_db_options.write_policy == TxnDBWritePolicy::WRITE_COMMITTED) {
|
|
// Consume one seq per key
|
|
exp_seq += 3;
|
|
} else {
|
|
// Consume one seq per batch
|
|
exp_seq++;
|
|
// Consume one seq for commit
|
|
exp_seq++;
|
|
}
|
|
ASSERT_OK(s);
|
|
};
|
|
std::function<void(size_t)> txn_t2 = [&](size_t index) {
|
|
// Commit without prepare. It should write to DB without a commit marker.
|
|
TransactionOptions txn_options;
|
|
WriteOptions write_options;
|
|
Transaction* txn = db->BeginTransaction(write_options, txn_options);
|
|
auto istr = std::to_string(index);
|
|
auto s = txn->SetName("xid" + istr);
|
|
ASSERT_OK(s);
|
|
s = txn->Put(Slice("foo" + istr), Slice("bar"));
|
|
s = txn->Put(Slice("foo2" + istr), Slice("bar2"));
|
|
s = txn->Put(Slice("foo3" + istr), Slice("bar3"));
|
|
s = txn->Put(Slice("foo4" + istr), Slice("bar4"));
|
|
ASSERT_OK(s);
|
|
s = txn->Commit();
|
|
ASSERT_OK(s);
|
|
if (txn_db_options.write_policy == TxnDBWritePolicy::WRITE_COMMITTED) {
|
|
// Consume one seq per key
|
|
exp_seq += 4;
|
|
} else {
|
|
// Consume one seq per batch
|
|
exp_seq++;
|
|
// Consume one seq for commit
|
|
exp_seq++;
|
|
}
|
|
auto pdb = reinterpret_cast<PessimisticTransactionDB*>(db);
|
|
pdb->UnregisterTransaction(txn);
|
|
delete txn;
|
|
};
|
|
std::function<void(size_t)> txn_t3 = [&](size_t index) {
|
|
// A full 2pc txn that also involves a commit marker.
|
|
TransactionOptions txn_options;
|
|
WriteOptions write_options;
|
|
Transaction* txn = db->BeginTransaction(write_options, txn_options);
|
|
auto istr = std::to_string(index);
|
|
auto s = txn->SetName("xid" + istr);
|
|
ASSERT_OK(s);
|
|
s = txn->Put(Slice("foo" + istr), Slice("bar"));
|
|
s = txn->Put(Slice("foo2" + istr), Slice("bar2"));
|
|
s = txn->Put(Slice("foo3" + istr), Slice("bar3"));
|
|
s = txn->Put(Slice("foo4" + istr), Slice("bar4"));
|
|
s = txn->Put(Slice("foo5" + istr), Slice("bar5"));
|
|
ASSERT_OK(s);
|
|
expected_commits++;
|
|
s = txn->Prepare();
|
|
ASSERT_OK(s);
|
|
commit_writes++;
|
|
s = txn->Commit();
|
|
ASSERT_OK(s);
|
|
if (txn_db_options.write_policy == TxnDBWritePolicy::WRITE_COMMITTED) {
|
|
// Consume one seq per key
|
|
exp_seq += 5;
|
|
} else {
|
|
// Consume one seq per batch
|
|
exp_seq++;
|
|
// Consume one seq per commit marker
|
|
exp_seq++;
|
|
}
|
|
delete txn;
|
|
};
|
|
std::function<void(size_t)> txn_t4 = [&](size_t index) {
|
|
// A full 2pc txn that also involves a commit marker.
|
|
TransactionOptions txn_options;
|
|
WriteOptions write_options;
|
|
Transaction* txn = db->BeginTransaction(write_options, txn_options);
|
|
auto istr = std::to_string(index);
|
|
auto s = txn->SetName("xid" + istr);
|
|
ASSERT_OK(s);
|
|
s = txn->Put(Slice("foo" + istr), Slice("bar"));
|
|
s = txn->Put(Slice("foo2" + istr), Slice("bar2"));
|
|
s = txn->Put(Slice("foo3" + istr), Slice("bar3"));
|
|
s = txn->Put(Slice("foo4" + istr), Slice("bar4"));
|
|
s = txn->Put(Slice("foo5" + istr), Slice("bar5"));
|
|
ASSERT_OK(s);
|
|
expected_commits++;
|
|
s = txn->Prepare();
|
|
ASSERT_OK(s);
|
|
commit_writes++;
|
|
s = txn->Rollback();
|
|
ASSERT_OK(s);
|
|
if (txn_db_options.write_policy == TxnDBWritePolicy::WRITE_COMMITTED) {
|
|
// No seq is consumed for deleting the txn buffer
|
|
exp_seq += 0;
|
|
} else {
|
|
// Consume one seq per batch
|
|
exp_seq++;
|
|
// Consume one seq per rollback batch
|
|
exp_seq++;
|
|
// Consume one seq for rollback commit
|
|
exp_seq++;
|
|
}
|
|
delete txn;
|
|
};
|
|
|
|
// Test that we can change write policy after a clean shutdown (which would
|
|
// empty the WAL)
|
|
void CrossCompatibilityTest(TxnDBWritePolicy from_policy,
|
|
TxnDBWritePolicy to_policy, bool empty_wal) {
|
|
TransactionOptions txn_options;
|
|
ReadOptions read_options;
|
|
WriteOptions write_options;
|
|
uint32_t index = 0;
|
|
Random rnd(1103);
|
|
options.write_buffer_size = 1024; // To create more sst files
|
|
std::unordered_map<std::string, std::string> committed_kvs;
|
|
Transaction* txn;
|
|
|
|
txn_db_options.write_policy = from_policy;
|
|
ReOpen();
|
|
|
|
for (int i = 0; i < 1024; i++) {
|
|
auto istr = std::to_string(index);
|
|
auto k = Slice("foo-" + istr).ToString();
|
|
auto v = Slice("bar-" + istr).ToString();
|
|
// For test the duplicate keys
|
|
auto v2 = Slice("bar2-" + istr).ToString();
|
|
auto type = rnd.Uniform(4);
|
|
Status s;
|
|
switch (type) {
|
|
case 0:
|
|
committed_kvs[k] = v;
|
|
s = db->Put(write_options, k, v);
|
|
ASSERT_OK(s);
|
|
committed_kvs[k] = v2;
|
|
s = db->Put(write_options, k, v2);
|
|
ASSERT_OK(s);
|
|
break;
|
|
case 1: {
|
|
WriteBatch wb;
|
|
committed_kvs[k] = v;
|
|
wb.Put(k, v);
|
|
// TODO(myabandeh): remove this when we supprot duplicate keys in
|
|
// db->Write method
|
|
if (false) {
|
|
committed_kvs[k] = v2;
|
|
wb.Put(k, v2);
|
|
}
|
|
s = db->Write(write_options, &wb);
|
|
ASSERT_OK(s);
|
|
} break;
|
|
case 2:
|
|
case 3:
|
|
txn = db->BeginTransaction(write_options, txn_options);
|
|
s = txn->SetName("xid" + istr);
|
|
ASSERT_OK(s);
|
|
committed_kvs[k] = v;
|
|
s = txn->Put(k, v);
|
|
ASSERT_OK(s);
|
|
// TODO(myabandeh): remove this when we supprot duplicate keys in
|
|
// db->Write method
|
|
if (false) {
|
|
committed_kvs[k] = v2;
|
|
s = txn->Put(k, v2);
|
|
}
|
|
ASSERT_OK(s);
|
|
if (type == 3) {
|
|
s = txn->Prepare();
|
|
ASSERT_OK(s);
|
|
}
|
|
s = txn->Commit();
|
|
ASSERT_OK(s);
|
|
if (type == 2) {
|
|
auto pdb = reinterpret_cast<PessimisticTransactionDB*>(db);
|
|
// TODO(myabandeh): this is counter-intuitive. The destructor should
|
|
// also do the unregistering.
|
|
pdb->UnregisterTransaction(txn);
|
|
}
|
|
delete txn;
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
|
|
index++;
|
|
} // for i
|
|
|
|
txn_db_options.write_policy = to_policy;
|
|
auto db_impl = reinterpret_cast<DBImpl*>(db->GetRootDB());
|
|
// Before upgrade/downgrade the WAL must be emptied
|
|
if (empty_wal) {
|
|
db_impl->TEST_FlushMemTable();
|
|
} else {
|
|
db_impl->FlushWAL(true);
|
|
}
|
|
auto s = ReOpenNoDelete();
|
|
if (empty_wal) {
|
|
ASSERT_OK(s);
|
|
} else {
|
|
// Test that we can detect the WAL that is produced by an incompatbile
|
|
// WritePolicy and fail fast before mis-interpreting the WAL.
|
|
ASSERT_TRUE(s.IsNotSupported());
|
|
return;
|
|
}
|
|
db_impl = reinterpret_cast<DBImpl*>(db->GetRootDB());
|
|
// Check that WAL is empty
|
|
VectorLogPtr log_files;
|
|
db_impl->GetSortedWalFiles(log_files);
|
|
ASSERT_EQ(0, log_files.size());
|
|
|
|
for (auto& kv : committed_kvs) {
|
|
std::string value;
|
|
s = db->Get(read_options, kv.first, &value);
|
|
if (s.IsNotFound()) {
|
|
printf("key = %s\n", kv.first.c_str());
|
|
}
|
|
ASSERT_OK(s);
|
|
if (kv.second != value) {
|
|
printf("key = %s\n", kv.first.c_str());
|
|
}
|
|
ASSERT_EQ(kv.second, value);
|
|
}
|
|
}
|
|
};
|
|
|
|
class MySQLStyleTransactionTest : public TransactionTest {};
|
|
|
|
} // namespace rocksdb
|