Skip to content

Commit 3a26c8b

Browse files
committed
kv: harden test transaction 2pc core
1 parent 48009c6 commit 3a26c8b

3 files changed

Lines changed: 200 additions & 19 deletions

File tree

include/pingcap/kv/2pc.h

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include <cmath>
1010
#include <memory>
11+
#include <shared_mutex>
1112
#include <thread>
1213
#include <unordered_map>
1314
#include <utility>
@@ -98,6 +99,7 @@ struct TwoPhaseCommitter : public std::enable_shared_from_this<TwoPhaseCommitter
9899
Cluster * cluster;
99100

100101
std::unordered_map<uint64_t, int> region_txn_size;
102+
// Total bytes of all keys and values in this transaction. Used for lock TTL decisions.
101103
uint64_t txn_size = 0;
102104

103105
int lock_ttl = 0;
@@ -106,6 +108,8 @@ struct TwoPhaseCommitter : public std::enable_shared_from_this<TwoPhaseCommitter
106108
// commited means primary key has been written to kv stores.
107109
bool commited;
108110

111+
bool commit_result_undetermined = false;
112+
109113
// Only for test now
110114
bool use_async_commit;
111115

@@ -148,6 +152,8 @@ struct TwoPhaseCommitter : public std::enable_shared_from_this<TwoPhaseCommitter
148152

149153
void commitKeys(Backoffer & bo, const std::vector<std::string> & keys) { doActionOnKeys<ActionCommit>(bo, keys); }
150154

155+
void cleanupKeys(Backoffer & bo, const std::vector<std::string> & keys) { doActionOnKeys<ActionCleanUp>(bo, keys); }
156+
151157
template <Action action>
152158
void doActionOnKeys(Backoffer & bo, const std::vector<std::string> & cur_keys)
153159
{
@@ -178,9 +184,12 @@ struct TwoPhaseCommitter : public std::enable_shared_from_this<TwoPhaseCommitter
178184
batches.emplace_back(BatchKeys(group.first, sub_keys));
179185
}
180186
}
181-
if (primary_idx != std::numeric_limits<uint64_t>::max() && primary_idx != 0)
187+
if (primary_idx != std::numeric_limits<uint64_t>::max())
182188
{
183-
std::swap(batches[0], batches[primary_idx]);
189+
if (primary_idx != 0)
190+
{
191+
std::swap(batches[0], batches[primary_idx]);
192+
}
184193
batches[0].is_primary = true;
185194
}
186195

@@ -213,12 +222,18 @@ struct TwoPhaseCommitter : public std::enable_shared_from_this<TwoPhaseCommitter
213222
{
214223
commitSingleBatch(bo, batch);
215224
}
225+
else if constexpr (action == ActionCleanUp)
226+
{
227+
cleanupSingleBatch(bo, batch);
228+
}
216229
}
217230
}
218231

219232
void prewriteSingleBatch(Backoffer & bo, const BatchKeys & batch);
220233

221234
void commitSingleBatch(Backoffer & bo, const BatchKeys & batch);
235+
236+
void cleanupSingleBatch(Backoffer & bo, const BatchKeys & batch);
222237
};
223238

224239
} // namespace kv

src/kv/2pc.cc

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ uint64_t txnLockTTL(std::chrono::milliseconds start, uint64_t txn_size)
2121

2222
if (txn_size >= txnCommitBatchSize)
2323
{
24-
uint64_t txn_size_mb = txn_size / bytesPerMiB;
24+
double txn_size_mb = static_cast<double>(txn_size) / bytesPerMiB;
2525
lock_ttl = static_cast<uint64_t>(ttlFactor * sqrt(txn_size_mb));
2626
if (lock_ttl < defaultLockTTL)
2727
{
@@ -47,15 +47,15 @@ TwoPhaseCommitter::TwoPhaseCommitter(Txn * txn, bool _use_async_commit)
4747
txn->walkBuffer([&](const std::string & key, const std::string & value) {
4848
keys.push_back(key);
4949
mutations.emplace(key, value);
50+
txn_size += key.size() + value.size();
5051
});
5152
cluster = txn->cluster;
5253
start_ts = txn->start_ts;
53-
primary_lock = keys[0];
54-
txn_size = mutations.size();
55-
// TODO: use right lock_ttl
56-
// currently prewrite is not concurrent, so the right lock_ttl is not enough for prewrite to complete
57-
// lock_ttl = txnLockTTL(txn->start_time, txn_size);
58-
lock_ttl = defaultLockTTL;
54+
if (!keys.empty())
55+
{
56+
primary_lock = keys[0];
57+
}
58+
lock_ttl = txnLockTTL(txn->start_time, txn_size);
5959
if (txn_size > ttlManagerRunThreshold)
6060
{
6161
lock_ttl = managedLockTTL;
@@ -66,6 +66,11 @@ void TwoPhaseCommitter::execute()
6666
{
6767
try
6868
{
69+
if (keys.empty())
70+
{
71+
return;
72+
}
73+
6974
if (use_async_commit)
7075
{
7176
// If we want to use async commit or 1PC and also want external consistency across
@@ -103,15 +108,28 @@ void TwoPhaseCommitter::execute()
103108
// TODO: check expired
104109
Backoffer commit_bo(commitMaxBackoff);
105110
commitKeys(commit_bo, keys);
106-
// TODO: Process commit exception
107111

108112
ttl_manager.close();
109113
}
110114
catch (Exception & e)
111115
{
112-
if (!commited)
116+
ttl_manager.close();
117+
if (commited)
113118
{
114-
// TODO: Rollback keys.
119+
log->warning("write commit exception after primary committed: " + e.displayText());
120+
return;
121+
}
122+
if (!commit_result_undetermined)
123+
{
124+
try
125+
{
126+
Backoffer cleanup_bo(cleanupMaxBackoff);
127+
cleanupKeys(cleanup_bo, keys);
128+
}
129+
catch (Exception & cleanup_error)
130+
{
131+
log->warning("2PC cleanup exception: " + cleanup_error.displayText());
132+
}
115133
}
116134
log->warning("write commit exception: " + e.displayText());
117135
throw;
@@ -210,7 +228,7 @@ void TwoPhaseCommitter::prewriteSingleBatch(Backoffer & bo, const BatchKeys & ba
210228
}
211229
else
212230
{
213-
if (batch.keys[0] == primary_lock)
231+
if (batch.is_primary)
214232
{
215233
// After writing the primary key, if the size of the transaction is large than 32M,
216234
// start the ttlManager. The ttlManager will be closed in tikvTxn.Commit().
@@ -261,19 +279,65 @@ void TwoPhaseCommitter::commitSingleBatch(Backoffer & bo, const BatchKeys & batc
261279
}
262280
catch (Exception & e)
263281
{
282+
if (batch.is_primary && e.code() != RegionEpochNotMatch)
283+
{
284+
commit_result_undetermined = true;
285+
throw;
286+
}
264287
bo.backoff(boRegionMiss, e);
265-
commit_ts = cluster->pd_client->getTS();
266288
commitKeys(bo, batch.keys);
267289
return;
268290
}
269291
if (response.has_error())
270292
{
293+
if (response.error().has_commit_ts_expired())
294+
{
295+
const auto & rejected = response.error().commit_ts_expired();
296+
if (rejected.min_commit_ts() > rejected.attempted_commit_ts()
297+
&& rejected.min_commit_ts() - rejected.attempted_commit_ts() > (3600000ULL << pd::physicalShiftBits))
298+
{
299+
throw Exception("2PC MinCommitTS is too large, got MinCommitTS: " + std::to_string(rejected.min_commit_ts())
300+
+ ", AttemptedCommitTS: " + std::to_string(rejected.attempted_commit_ts()),
301+
LockError);
302+
}
303+
commit_ts = cluster->pd_client->getTS();
304+
req.set_commit_version(commit_ts);
305+
commitSingleBatch(bo, batch);
306+
return;
307+
}
271308
throw Exception("meet errors: " + response.error().ShortDebugString(), LockError);
272309
}
273310

274311
commited = true;
275312
}
276313

314+
void TwoPhaseCommitter::cleanupSingleBatch(Backoffer & bo, const BatchKeys & batch)
315+
{
316+
kvrpcpb::BatchRollbackRequest req;
317+
for (const auto & key : batch.keys)
318+
{
319+
req.add_keys(key);
320+
}
321+
req.set_start_version(start_ts);
322+
323+
kvrpcpb::BatchRollbackResponse response;
324+
RegionClient region_client(cluster, batch.region);
325+
try
326+
{
327+
region_client.sendReqToRegion<RPC_NAME(KvBatchRollback)>(bo, req, &response);
328+
}
329+
catch (Exception & e)
330+
{
331+
bo.backoff(boRegionMiss, e);
332+
cleanupKeys(bo, batch.keys);
333+
return;
334+
}
335+
if (response.has_error())
336+
{
337+
throw Exception("meet cleanup errors: " + response.error().ShortDebugString(), LockError);
338+
}
339+
}
340+
277341
uint64_t sendTxnHeartBeat(Backoffer & bo, Cluster * cluster, std::string & primary_key, uint64_t start_ts, uint64_t ttl)
278342
{
279343
for (;;)

src/test/real_tikv_test/2pc_test.cc

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ struct TestTwoPhaseCommitter
1919
TwoPhaseCommitterPtr committer;
2020

2121
public:
22-
TestTwoPhaseCommitter(Txn * txn) : committer(std::make_shared<TwoPhaseCommitter>(txn)) {}
22+
TestTwoPhaseCommitter(Txn * txn)
23+
: committer(std::make_shared<TwoPhaseCommitter>(txn))
24+
{}
2325

2426
void prewriteKeys(Backoffer & bo, const std::vector<std::string> & keys) { committer->prewriteKeys(bo, keys); }
2527

@@ -28,6 +30,12 @@ struct TestTwoPhaseCommitter
2830
std::vector<std::string> keys() { return committer->keys; }
2931

3032
void setCommitTS(int64_t commit_ts) { committer->commit_ts = commit_ts; }
33+
34+
uint64_t startTS() { return committer->start_ts; }
35+
36+
uint64_t txnSize() { return committer->txn_size; }
37+
38+
int lockTTL() { return committer->lock_ttl; }
3139
};
3240

3341
} // namespace kv
@@ -71,7 +79,6 @@ class TestWith2PCRealTiKV : public testing::Test
7179

7280
TEST_F(TestWith2PCRealTiKV, testCommitRollback)
7381
{
74-
7582
// Commit.
7683
{
7784
Txn txn(test_cluster.get());
@@ -97,7 +104,7 @@ TEST_F(TestWith2PCRealTiKV, testCommitRollback)
97104
txn2.set("c", "c2");
98105
txn2.commit();
99106

100-
txn1.commit();
107+
ASSERT_THROW(txn1.commit(), Exception);
101108

102109
Snapshot snap(test_cluster.get());
103110
ASSERT_EQ(snap.Get("a"), "a");
@@ -106,9 +113,104 @@ TEST_F(TestWith2PCRealTiKV, testCommitRollback)
106113
}
107114
}
108115

109-
TEST_F(TestWith2PCRealTiKV, commitAfterReadByOtherTxn)
116+
TEST_F(TestWith2PCRealTiKV, testEmptyTxnCommit)
117+
{
118+
Txn txn(test_cluster.get());
119+
ASSERT_NO_THROW(txn.commit());
120+
}
121+
122+
TEST_F(TestWith2PCRealTiKV, testCommitTsExpiredRetries)
123+
{
124+
const std::string prefix = "clientc_commit_ts_expired_" + std::to_string(test_cluster->pd_client->getTS()) + "_";
125+
const std::string key = prefix + "k";
126+
127+
{
128+
Txn txn(test_cluster.get());
129+
txn.set(key, "v0");
130+
txn.commit();
131+
}
132+
133+
Txn txn(test_cluster.get());
134+
txn.set(key, "v1");
135+
TestTwoPhaseCommitter committer{&txn};
136+
Backoffer prewrite_bo(prewriteMaxBackoff);
137+
committer.prewriteKeys(prewrite_bo, committer.keys());
138+
139+
// A reader after prewrite pushes the lock's min_commit_ts. Commit with a
140+
// stale commit_ts should be rejected by TiKV with CommitTsExpired, and the
141+
// client should retry the same primary commit with a fresh commit_ts.
142+
Txn reader(test_cluster.get());
143+
auto result = reader.get(key);
144+
ASSERT_EQ(result.second, true);
145+
ASSERT_EQ(result.first, "v0");
146+
147+
committer.setCommitTS(committer.startTS() + 1);
148+
Backoffer commit_bo(commitMaxBackoff);
149+
ASSERT_NO_THROW(committer.commitKeys(commit_bo, committer.keys()));
150+
151+
Snapshot snap(test_cluster.get());
152+
ASSERT_EQ(snap.Get(key), "v1");
153+
}
154+
155+
TEST_F(TestWith2PCRealTiKV, testFailedPrewriteCleansWrittenLocks)
156+
{
157+
const std::string prefix = "clientc_cleanup_" + std::to_string(test_cluster->pd_client->getTS()) + "_";
158+
const std::string key_a = prefix + "a";
159+
const std::string key_b = prefix + "b";
160+
const std::string key_c = prefix + "c";
161+
162+
{
163+
Txn txn(test_cluster.get());
164+
txn.set(key_a, "a0");
165+
txn.set(key_b, "b0");
166+
txn.set(key_c, "c0");
167+
txn.commit();
168+
}
169+
170+
test_cluster->splitRegion(key_c);
171+
172+
Txn older_writer(test_cluster.get());
173+
older_writer.set(key_a, "a1");
174+
older_writer.set(key_b, "b1");
175+
older_writer.set(key_c, "c1");
176+
177+
{
178+
Txn newer_writer(test_cluster.get());
179+
newer_writer.set(key_c, "c2");
180+
newer_writer.commit();
181+
}
182+
183+
ASSERT_THROW(older_writer.commit(), Exception);
184+
185+
Backoffer mvcc_bo(GetMaxBackoff);
186+
ASSERT_FALSE(Snapshot(test_cluster.get()).mvccGet(mvcc_bo, key_a).has_lock());
187+
ASSERT_FALSE(Snapshot(test_cluster.get()).mvccGet(mvcc_bo, key_b).has_lock());
188+
189+
{
190+
Txn next_writer(test_cluster.get());
191+
next_writer.set(key_a, "a2");
192+
next_writer.set(key_b, "b2");
193+
ASSERT_NO_THROW(next_writer.commit());
194+
}
195+
196+
Snapshot snap(test_cluster.get());
197+
ASSERT_EQ(snap.Get(key_a), "a2");
198+
ASSERT_EQ(snap.Get(key_b), "b2");
199+
ASSERT_EQ(snap.Get(key_c), "c2");
200+
}
201+
202+
TEST_F(TestWith2PCRealTiKV, testLargeTxnTTLUsesBytes)
110203
{
204+
Txn txn(test_cluster.get());
205+
txn.set("clientc_large_ttl_key", std::string(33 * 1024 * 1024, 'x'));
206+
TestTwoPhaseCommitter committer{&txn};
207+
208+
ASSERT_GT(committer.txnSize(), 32ULL * 1024 * 1024);
209+
ASSERT_EQ(committer.lockTTL(), 20000);
210+
}
111211

212+
TEST_F(TestWith2PCRealTiKV, commitAfterReadByOtherTxn)
213+
{
112214
// Commit.
113215
{
114216
Txn txn(test_cluster.get());
@@ -337,4 +439,4 @@ TEST_F(TestWith2PCRealTiKV, testScanWithLargeTxn)
337439
}
338440
}
339441

340-
} // namespace
442+
} // namespace pingcap::tests

0 commit comments

Comments
 (0)