This is an automated email from the ASF dual-hosted git repository.
bobhan1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 97ae75cb12f [fix](cloud) Resume committed transactions through lazy
publication (#69606)
97ae75cb12f is described below
commit 97ae75cb12f532df0ee42da4bc288559bd19b8a3
Author: bobhan1 <[email protected]>
AuthorDate: Tue Oct 27 10:10:24 2026 +0800
[fix](cloud) Resume committed transactions through lazy publication (#69606)
### What problem does this PR solve?
Related PR: #67989
A transaction commit retry can scan the temporary rowsets left by
concurrent lazy publication, then start its write transaction after
those rowsets and their pending markers have been removed. The
transaction is still COMMITTED, so processing the old scan can publish
the same rowsets again or return only part of the transaction's
partition versions.
Handle COMMITTED explicitly in the immediate, eventual and
sub-transaction commit paths through a shared wait_for_committed_txn
helper. After the non-snapshot txn_info read in the actual write
transaction, release that transaction and submit or join the lazy task.
After publication succeeds, restart and return the durable VISIBLE
transaction through the existing idempotent branch, with empty version
results so FE invalidates its version caches. A concurrent transition
from PREPARED to COMMITTED conflicts with the same txn_info read at KV
commit.
If the lazy task fails, the transaction has already committed and the
RPC still returns OK. Match the existing lazy response contract: return
txn_info as VISIBLE for FE callbacks, with is_lazy_commit=true and
is_lazy_commit_incomplete=true, without changing the durable COMMITTED
state or publishing the old scan. The two flags also identify incomplete
publication when the retry originally selected immediate, so FE does not
treat it as completed visibility.
Also clear the response before falling back from a failed immediate
attempt to lazy commit. Immediate may populate version results before
its KV commit fails with TXN_BYTES_TOO_LARGE; those results must not be
combined with the successful lazy response or retained in an
already-visible response.
#### Commit structure
1. Add three deterministic real-FDB retry tests and strengthen the
existing fallback response test.
2. Resume COMMITTED transactions through lazy publication in all three
commit paths, preserving commit success if publication fails.
3. Clear failed immediate response data before lazy fallback.
#### Validation
- Real-FDB retry tests cover an immediate retry with a stale scan, a
retry that still selects lazy, and publication failure after the
transaction has committed. The failure case checks both immediate and
lazy retry responses: OK, incomplete-lazy flags, no version/statistics
hints, unchanged business metadata and durable COMMITTED status.
- The immediate regression reproduced real version-3 duplicates on the
base implementation. The lazy regression fails before its existing
DCHECK aborts the whole test binary. The existing MemTxnKv fallback test
rejects duplicate version results from a failed immediate attempt.
- Final focused ASAN run: **all four related cases passed, zero
failures/errors**. A complete-file rerun exited 137 during
ConcurrentCommitTxnEventuallyCase3Test before producing an XML report;
no assertion failure was logged before interruption, and this run is not
counted as passing. FDB 7.1.57 was deployed as a private instance with
the repository's tools/fdb/fdb_ctl.sh; its processes were stopped and
temporary configuration restored afterward.
- Changed-line clang-format 16 checks and git diff --check passed. The
build-hygiene check reports an existing BE util/threadpool.h reach
overrun (1088 TUs versus a limit of 1087), also reproduced from archived
HEAD sources before this refactor; no BE files were changed. Clang-tidy
was not run.
The successful retry cases use bounded gates to synchronize actual
partition cleanup and final visibility, compare business metadata
including statistics, check rowset identities and versions, and verify
the empty VISIBLE response used by FE cache invalidation. These are MS
tests; the failure case does not run an FE process or independently
reproduce the sub-transaction COMMITTED retry timeline.
### Release note
Prevent duplicate publication and incomplete version responses when
transaction commit retries race lazy publication, while preserving
commit success when lazy publication is incomplete.
### Check List (For Author)
- Test
- [x] Unit Test
- Behavior changed:
- [x] Yes. Resume lazy publication for COMMITTED retries, preserve
commit success on publication failure, and discard failed immediate
response data before fallback.
- Does this need documentation?
- [x] No. No new user-facing setting or syntax.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
cloud/src/meta-service/meta_service.h | 4 +
cloud/src/meta-service/meta_service_txn.cpp | 55 ++++
cloud/src/meta-service/txn_lazy_committer.cpp | 3 +-
cloud/test/txn_lazy_commit_test.cpp | 359 ++++++++++++++++++++++++++
4 files changed, 420 insertions(+), 1 deletion(-)
diff --git a/cloud/src/meta-service/meta_service.h
b/cloud/src/meta-service/meta_service.h
index 7023ccf56f6..368676c9cbd 100644
--- a/cloud/src/meta-service/meta_service.h
+++ b/cloud/src/meta-service/meta_service.h
@@ -490,6 +490,10 @@ private:
bool is_version_read_enabled(std::string_view instance_id) const;
bool is_version_write_enabled(std::string_view instance_id) const;
+ bool wait_for_committed_txn(const std::string& instance_id, const
TxnInfoPB& txn_info,
+ std::unique_ptr<Transaction>& txn,
CommitTxnResponse* response,
+ MetaServiceCode& code, std::string& msg,
KVStats& stats);
+
void commit_txn_immediately(
const CommitTxnRequest* request, CommitTxnResponse* response,
MetaServiceCode& code,
std::string& msg, const std::string& instance_id, int64_t db_id,
diff --git a/cloud/src/meta-service/meta_service_txn.cpp
b/cloud/src/meta-service/meta_service_txn.cpp
index caa967f2ee7..2b71443e008 100644
--- a/cloud/src/meta-service/meta_service_txn.cpp
+++ b/cloud/src/meta-service/meta_service_txn.cpp
@@ -1840,6 +1840,36 @@ std::pair<MetaServiceCode, std::string>
get_partition_versions(
return {MetaServiceCode::OK, ""};
}
+// Returns true when the caller should reread VISIBLE.
+bool MetaServiceImpl::wait_for_committed_txn(const std::string& instance_id,
+ const TxnInfoPB& txn_info,
+ std::unique_ptr<Transaction>& txn,
+ CommitTxnResponse* response,
MetaServiceCode& code,
+ std::string& msg, KVStats& stats)
{
+ // Lazy publication owns this transaction; do not reuse its stale rowset
scan.
+ stats.get_bytes += txn->get_bytes();
+ stats.get_counter += txn->num_get_keys();
+ txn.reset();
+ auto txn_id = txn_info.txn_id();
+ auto task = txn_lazy_committer_->submit(instance_id, txn_id);
+ TEST_SYNC_POINT_CALLBACK("commit_txn::wait_committed", &txn_id);
+ std::tie(code, msg) = task->wait();
+ if (code == MetaServiceCode::OK) {
+ return true;
+ }
+
+ LOG(WARNING) << "txn lazy commit failed txn_id=" << txn_id << " code=" <<
code
+ << " msg=" << msg;
+ // The transaction is committed; match the incomplete lazy response
contract.
+ code = MetaServiceCode::OK;
+ msg.clear();
+ response->mutable_txn_info()->CopyFrom(txn_info);
+ response->mutable_txn_info()->set_status(TxnStatusPB::TXN_STATUS_VISIBLE);
+ response->set_is_lazy_commit(true);
+ response->set_is_lazy_commit_incomplete(true);
+ return false;
+}
+
/**
* 0. Extract txn_id from request
* 1. Get db id from TxnKv with txn_id
@@ -1957,6 +1987,13 @@ void MetaServiceImpl::commit_txn_immediately(
return;
}
+ if (txn_info.status() == TxnStatusPB::TXN_STATUS_COMMITTED) {
+ if (!wait_for_committed_txn(instance_id, txn_info, txn, response,
code, msg, stats)) {
+ return;
+ }
+ continue;
+ }
+
if (request->enable_check_commit_tso_fence() &&
config::enable_check_commit_tso_fence &&
txn_info.status() != TxnStatusPB::TXN_STATUS_COMMITTED &&
!check_txn_commit_tso_fence(txn.get(), instance_id, commit_tso,
response, code, msg)) {
@@ -2823,6 +2860,13 @@ void MetaServiceImpl::commit_txn_eventually(
return;
}
+ if (txn_info.status() == TxnStatusPB::TXN_STATUS_COMMITTED) {
+ if (!wait_for_committed_txn(instance_id, txn_info, txn, response,
code, msg, stats)) {
+ return;
+ }
+ continue;
+ }
+
if (request->enable_check_commit_tso_fence() &&
config::enable_check_commit_tso_fence &&
txn_info.status() != TxnStatusPB::TXN_STATUS_COMMITTED &&
!check_txn_commit_tso_fence(txn.get(), instance_id, commit_tso,
response, code, msg)) {
@@ -2844,6 +2888,7 @@ void MetaServiceImpl::commit_txn_eventually(
if (request->has_commit_attachment()) {
txn_info.mutable_commit_attachment()->CopyFrom(request->commit_attachment());
}
+
TEST_SYNC_POINT_RETURN_WITH_VOID("commit_txn_eventually::before_mark_committed",
&code);
DCHECK(txn_info.status() != TxnStatusPB::TXN_STATUS_COMMITTED);
// set status TXN_STATUS_COMMITTED not TXN_STATUS_VISIBLE !!!
// lazy commit task will advance txn to make txn visible
@@ -3229,6 +3274,13 @@ void MetaServiceImpl::commit_txn_with_sub_txn(const
CommitTxnRequest* request,
return;
}
+ if (txn_info.status() == TxnStatusPB::TXN_STATUS_COMMITTED) {
+ if (!wait_for_committed_txn(instance_id, txn_info, txn, response,
code, msg, stats)) {
+ return;
+ }
+ continue;
+ }
+
if (request->enable_check_commit_tso_fence() &&
config::enable_check_commit_tso_fence &&
txn_info.status() != TxnStatusPB::TXN_STATUS_COMMITTED &&
!check_txn_commit_tso_fence(txn.get(), instance_id, commit_tso,
response, code, msg)) {
@@ -3825,6 +3877,9 @@ void
MetaServiceImpl::commit_txn(::google::protobuf::RpcController* controller,
<< " tmp_rowsets_meta.size=" << tmp_rowsets_meta.size();
code = MetaServiceCode::OK;
msg.clear();
+ // Drop failed immediate results so FE's
CloudGlobalTransactionMgr.updateVersion()
+ // can invalidate caches when lazy returns only VISIBLE txn_info.
+ response->Clear();
commit_txn_eventually(request, response, code, msg, instance_id, db_id,
tmp_rowsets_meta,
stats);
}
diff --git a/cloud/src/meta-service/txn_lazy_committer.cpp
b/cloud/src/meta-service/txn_lazy_committer.cpp
index bec4b13f068..382cbb06963 100644
--- a/cloud/src/meta-service/txn_lazy_committer.cpp
+++ b/cloud/src/meta-service/txn_lazy_committer.cpp
@@ -918,7 +918,7 @@ std::pair<MetaServiceCode, std::string>
TxnLazyCommitTask::commit_partition(
LOG(INFO) << "remove tmp_rowset_key=" << hex(tmp_rowset_key) << "
txn_id=" << txn_id_;
}
- TEST_SYNC_POINT_CALLBACK("TxnLazyCommitter::commit");
+ TEST_SYNC_POINT_CALLBACK("TxnLazyCommitter::commit", &partition_id);
err = txn->commit();
if (err != TxnErrorCode::TXN_OK) {
code = cast_as<ErrCategory::COMMIT>(err);
@@ -927,6 +927,7 @@ std::pair<MetaServiceCode, std::string>
TxnLazyCommitTask::commit_partition(
return {code, msg};
}
}
+ TEST_SYNC_POINT_CALLBACK("TxnLazyCommitTask::commit_partition::finish",
&partition_id);
return {MetaServiceCode::OK, ""};
}
diff --git a/cloud/test/txn_lazy_commit_test.cpp
b/cloud/test/txn_lazy_commit_test.cpp
index e7875171892..e17056b61bc 100644
--- a/cloud/test/txn_lazy_commit_test.cpp
+++ b/cloud/test/txn_lazy_commit_test.cpp
@@ -24,11 +24,14 @@
#include <gtest/gtest.h>
#include <atomic>
+#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <limits>
+#include <map>
#include <memory>
+#include <mutex>
#include <random>
#include <string>
#include <thread>
@@ -1450,6 +1453,354 @@ TEST(TxnLazyCommitTest,
CommitTxnWithSubTxnRepairTabletIndexTest) {
}
}
+// Retry tests use a real lazy publisher and real FDB transactions. Two
partitions
+// have two tablets each, so a retry can scan a strict subset of the original
load.
+class CommittedTxnRetryTest : public testing::Test {
+protected:
+ using Metadata = std::map<std::string, std::string>;
+ static constexpr int64_t DB_ID = 93001;
+ static constexpr int64_t TABLE_ID = 93002;
+ static constexpr int64_t INDEX_ID = 93003;
+ static constexpr int64_t PARTITION_ID = 93004;
+ static constexpr int64_t TABLET_ID = 93100;
+ static constexpr auto INSTANCE_ID = "test_instance";
+
+ void SetUp() override {
+ config::txn_lazy_commit_rowsets_thresold = 2;
+ config::enable_cloud_parallel_txn_lazy_commit = true;
+ // Both partition workers must be able to reach their synchronization
point.
+ config::parallel_txn_lazy_commit_num_threads = 2;
+ config::cloud_txn_lazy_commit_fuzzy_possibility = 0;
+ kv_ = get_fdb_txn_kv();
+ service_ = get_meta_service(kv_, true);
+ sp_->enable_processing();
+
+ BeginTxnRequest begin;
+ begin.set_cloud_unique_id("test_cloud_unique_id");
+ auto* info = begin.mutable_txn_info();
+ info->set_db_id(DB_ID);
+ info->set_label("committed_txn_retry");
+ info->add_table_ids(TABLE_ID);
+ info->set_timeout_ms(60000);
+ BeginTxnResponse response;
+ brpc::Controller controller;
+ service_->begin_txn(&controller, &begin, &response, nullptr);
+ ASSERT_EQ(response.status().code(), MetaServiceCode::OK);
+ request_.set_cloud_unique_id("test_cloud_unique_id");
+ request_.set_db_id(DB_ID);
+ request_.set_txn_id(response.txn_id());
+ request_.set_is_2pc(false);
+ request_.set_enable_txn_lazy_commit(true);
+
+ for (int i = 0; i < 4; ++i) {
+ create_tablet_with_db_id(service_.get(), DB_ID, TABLE_ID, INDEX_ID,
+ PARTITION_ID + i / 2, TABLET_ID + i);
+ auto rowset =
+ create_rowset(request_.txn_id(), TABLET_ID + i, INDEX_ID,
PARTITION_ID + i / 2);
+ rowset.set_num_rows(7 + i);
+ rowset.set_num_segments(1);
+ rowset.set_data_disk_size(100);
+ rowset.set_index_disk_size(20);
+ rowset.set_total_disk_size(120);
+ CreateRowsetResponse result;
+ prepare_rowset(service_.get(), rowset, result);
+ ASSERT_EQ(result.status().code(), MetaServiceCode::OK);
+ commit_rowset(service_.get(), rowset, result);
+ ASSERT_EQ(result.status().code(), MetaServiceCode::OK);
+ rowsets_.push_back(std::move(rowset));
+ }
+ }
+
+ void TearDown() override {
+ // Release workers before joining, including when a test exits via
ASSERT.
+ // Keep callbacks and their captured fixture state alive until all
work ends.
+ {
+ std::lock_guard lock(mutex_);
+ stop_ = true;
+ cv_.notify_all();
+ }
+ join_requests();
+ service_.reset();
+ sp_->clear_all_call_backs();
+ sp_->clear_trace();
+ sp_->disable_processing();
+ config::txn_lazy_commit_rowsets_thresold = old_threshold_;
+ config::enable_cloud_parallel_txn_lazy_commit = old_parallel_;
+ config::parallel_txn_lazy_commit_num_threads = old_parallel_threads_;
+ config::cloud_txn_lazy_commit_fuzzy_possibility = old_fuzzy_;
+ }
+
+ CommitTxnResponse commit(const CommitTxnRequest& request) {
+ CommitTxnResponse response;
+ brpc::Controller controller;
+ service_->commit_txn(&controller, &request, &response, nullptr);
+ return response;
+ }
+
+ template <typename Predicate>
+ bool await(Predicate predicate) {
+ std::unique_lock lock(mutex_);
+ return cv_.wait_for(lock, std::chrono::seconds(10), [&] { return stop_
|| predicate(); }) &&
+ !stop_;
+ }
+
+ void start_partial_lazy_commit() {
+ sp_->set_call_back("TxnLazyCommitter::commit", [&](auto&& args) {
+ auto partition_id = *try_any_cast<int64_t*>(args[0]);
+ std::unique_lock lock(mutex_);
+ ++partitions_entered_;
+ cv_.notify_all();
+ if (partition_id == PARTITION_ID + 1) {
+ EXPECT_TRUE(cv_.wait_for(lock, std::chrono::seconds(10),
+ [&] { return stop_ || release_last_;
}));
+ }
+ });
+ sp_->set_call_back("TxnLazyCommitTask::commit_partition::finish",
[&](auto&& args) {
+ if (*try_any_cast<int64_t*>(args[0]) == PARTITION_ID) {
+ std::lock_guard lock(mutex_);
+ first_finished_ = true;
+ cv_.notify_all();
+ }
+ });
+
sp_->set_call_back("TxnLazyCommitTask::make_committed_txn_visible::commit",
[&](auto&&) {
+ std::unique_lock lock(mutex_);
+ before_visible_ = true;
+ cv_.notify_all();
+ EXPECT_TRUE(cv_.wait_for(lock, std::chrono::seconds(10),
+ [&] { return stop_ || release_visible_;
}));
+ });
+ sp_->set_call_back("commit_txn::wait_committed", [&](auto&& args) {
+ EXPECT_EQ(*try_any_cast<int64_t*>(args[0]), request_.txn_id());
+ std::lock_guard lock(mutex_);
+ retry_waiting_ = true;
+ cv_.notify_all();
+ });
+ original_ = std::thread([&] { original_response_ = commit(request_);
});
+ }
+
+ void finish_partition_before_retry() {
+ {
+ std::lock_guard lock(mutex_);
+ if (release_last_) return; // The restarted write transaction also
enters this hook.
+ release_last_ = true;
+ cv_.notify_all();
+ }
+ ASSERT_TRUE(await([&] { return before_visible_; }));
+ check_status(TxnStatusPB::TXN_STATUS_COMMITTED);
+ // Both partitions are published now, but the final visibility
transaction
+ // is paused. Save versions, rowsets and all statistics before the
retry.
+ before_retry_ = business_metadata();
+ }
+
+ void start_retry() {
+ retry_ = std::thread([&] {
+ retry_response_ = commit(request_);
+ std::lock_guard lock(mutex_);
+ retry_done_ = true;
+ cv_.notify_all();
+ });
+ }
+
+ void release_visibility() {
+ std::lock_guard lock(mutex_);
+ release_visible_ = true;
+ cv_.notify_all();
+ }
+
+ void join_requests() {
+ if (original_.joinable()) original_.join();
+ if (retry_.joinable()) retry_.join();
+ }
+
+ Metadata business_metadata() {
+ Metadata result;
+ std::unique_ptr<Transaction> txn;
+ EXPECT_EQ(kv_->create_txn(&txn), TxnErrorCode::TXN_OK);
+ FullRangeGetOptions options;
+ options.snapshot = true;
+ auto iter = txn->full_range_get("\x00", "\xfe", std::move(options));
+ for (auto item = iter->next(); item.has_value(); item = iter->next()) {
+ // The original request legitimately changes these three keys when
it
+ // becomes VISIBLE. All other keys must remain identical across
retry.
+ if (item->first == txn_info_key({INSTANCE_ID, DB_ID,
request_.txn_id()}) ||
+ item->first == txn_running_key({INSTANCE_ID, DB_ID,
request_.txn_id()}) ||
+ item->first == recycle_txn_key({INSTANCE_ID, DB_ID,
request_.txn_id()})) {
+ continue;
+ }
+ result.emplace(item->first, item->second);
+ }
+ EXPECT_TRUE(iter->is_valid()) << iter->error_code();
+ return result;
+ }
+
+ void check_status(TxnStatusPB expected) {
+ std::unique_ptr<Transaction> txn;
+ ASSERT_EQ(kv_->create_txn(&txn), TxnErrorCode::TXN_OK);
+ std::string value;
+ ASSERT_EQ(txn->get(txn_info_key({INSTANCE_ID, DB_ID,
request_.txn_id()}), &value),
+ TxnErrorCode::TXN_OK);
+ TxnInfoPB info;
+ ASSERT_TRUE(info.ParseFromString(value));
+ EXPECT_EQ(info.status(), expected);
+ }
+
+ void check_visible_retry() {
+ EXPECT_EQ(original_response_.status().code(), MetaServiceCode::OK);
+ ASSERT_EQ(retry_response_.status().code(), MetaServiceCode::OK)
+ << retry_response_.ShortDebugString();
+ EXPECT_EQ(retry_response_.txn_info().status(),
TxnStatusPB::TXN_STATUS_VISIBLE);
+ ASSERT_EQ(retry_response_.txn_info().table_ids_size(), 1);
+ EXPECT_EQ(retry_response_.txn_info().table_ids(0), TABLE_ID);
+ // This is the existing FE updateVersion() cache-invalidation response:
+ // VISIBLE, no partition/table versions, and no incomplete-lazy flag.
+ EXPECT_TRUE(retry_response_.partition_ids().empty());
+ EXPECT_TRUE(retry_response_.table_ids().empty());
+ EXPECT_TRUE(retry_response_.versions().empty());
+ EXPECT_TRUE(retry_response_.table_stats().empty());
+ EXPECT_FALSE(retry_response_.is_lazy_commit_incomplete());
+ EXPECT_EQ(business_metadata(), before_retry_);
+ check_status(TxnStatusPB::TXN_STATUS_VISIBLE);
+ std::unique_ptr<Transaction> txn;
+ ASSERT_EQ(kv_->create_txn(&txn), TxnErrorCode::TXN_OK);
+ for (int i = 0; i < 4; ++i) {
+ std::string value;
+ ASSERT_EQ(txn->get(meta_rowset_key({INSTANCE_ID, TABLET_ID + i,
2}), &value),
+ TxnErrorCode::TXN_OK);
+ RowsetMetaCloudPB rowset;
+ ASSERT_TRUE(rowset.ParseFromString(value));
+ EXPECT_EQ(rowset.rowset_id_v2(), rowsets_[i].rowset_id_v2());
+ EXPECT_EQ(rowset.start_version(), 2);
+ EXPECT_EQ(rowset.end_version(), 2);
+ EXPECT_EQ(rowset.num_rows(), rowsets_[i].num_rows());
+ check_rowset_meta_not_exist(txn, TABLET_ID + i, 3);
+ check_tmp_rowset_not_exist(txn, TABLET_ID + i, request_.txn_id());
+ }
+ }
+
+ std::shared_ptr<TxnKv> kv_;
+ std::unique_ptr<MetaServiceProxy> service_;
+ SyncPoint* sp_ = SyncPoint::get_instance();
+ CommitTxnRequest request_;
+ std::vector<RowsetMetaCloudPB> rowsets_;
+ Metadata before_retry_;
+ CommitTxnResponse original_response_;
+ CommitTxnResponse retry_response_;
+ std::thread original_;
+ std::thread retry_;
+ std::mutex mutex_;
+ std::condition_variable cv_;
+ int partitions_entered_ = 0;
+ bool first_finished_ = false;
+ bool release_last_ = false;
+ bool before_visible_ = false;
+ bool release_visible_ = false;
+ bool retry_waiting_ = false;
+ bool retry_done_ = false;
+ bool stop_ = false;
+
+private:
+ const int32_t old_threshold_ = config::txn_lazy_commit_rowsets_thresold;
+ const bool old_parallel_ = config::enable_cloud_parallel_txn_lazy_commit;
+ const int32_t old_parallel_threads_ =
config::parallel_txn_lazy_commit_num_threads;
+ const int32_t old_fuzzy_ = config::cloud_txn_lazy_commit_fuzzy_possibility;
+};
+
+TEST_F(CommittedTxnRetryTest, ImmediateRetryWaitsForVisibility) {
+ // 1. Lazy publishes both partitions; pause the second partition's tmp
cleanup.
+ // 2. Retry scans its two tmp rowsets and selects immediate (threshold =
2).
+ // 3. Finish cleanup before retry creates its write transaction, but hold
the
+ // global status at COMMITTED. Retry must wait rather than publish
version 3.
+ start_partial_lazy_commit();
+ ASSERT_TRUE(await([&] { return first_finished_ && partitions_entered_ ==
2; }));
+ sp_->set_call_back("commit_txn_immediately:begin",
+ [&](auto&&) { finish_partition_before_retry(); });
+ start_retry();
+ ASSERT_TRUE(await([&] { return retry_waiting_ || retry_done_; }));
+ {
+ std::lock_guard lock(mutex_);
+ EXPECT_TRUE(retry_waiting_);
+ EXPECT_FALSE(retry_done_);
+ }
+ release_visibility();
+ join_requests();
+ EXPECT_FALSE(retry_response_.is_lazy_commit());
+ check_visible_retry();
+}
+
+TEST_F(CommittedTxnRetryTest, LazyRetryWaitsForVisibility) {
+ config::txn_lazy_commit_rowsets_thresold = 1;
+ start_partial_lazy_commit();
+ ASSERT_TRUE(await([&] { return first_finished_ && partitions_entered_ ==
2; }));
+ // The same remaining two rowsets now select lazy. Remove the last pending
+ // marker before it reads versions, so the existing pending-wait path
cannot
+ // mask a missing COMMITTED-state check.
+ sp_->set_call_back("commit_txn_eventually:begin",
+ [&](auto&&) { finish_partition_before_retry(); });
+ sp_->set_call_back("commit_txn_eventually::before_mark_committed",
[&](auto&& args) {
+ // Stop the unfixed path before its DCHECK aborts the entire test
binary.
+ // Reaching this point is itself the failure: a committed retry is
about
+ // to change commit metadata again. The immediate case reproduces the
+ // actual duplicate FDB writes without injecting a conflict or error.
+ ADD_FAILURE() << "A committed retry must not mark the transaction
committed again";
+ *try_any_cast<MetaServiceCode*>(args[0]) =
MetaServiceCode::TXN_INVALID_STATUS;
+ *try_any_cast<bool*>(args.back()) = true;
+ });
+ start_retry();
+ ASSERT_TRUE(await([&] { return retry_waiting_ || retry_done_; }));
+ {
+ std::lock_guard lock(mutex_);
+ EXPECT_TRUE(retry_waiting_);
+ EXPECT_FALSE(retry_done_);
+ }
+ release_visibility();
+ join_requests();
+ EXPECT_TRUE(retry_response_.is_lazy_commit());
+ check_visible_retry();
+}
+
+TEST_F(CommittedTxnRetryTest, CommittedRetryReturnsSuccessWhenLazyFails) {
+
sp_->set_call_back("TxnLazyCommitTask::make_committed_txn_visible::commit",
[&](auto&& args) {
+ // Fail publication after the transaction has durably committed. Commit
+ // retries must preserve success while reporting incomplete lazy work.
+ *try_any_cast<MetaServiceCode*>(args[0]) =
MetaServiceCode::KV_TXN_COMMIT_ERR;
+ *try_any_cast<bool*>(args.back()) = true;
+ });
+ auto original = commit(request_);
+ ASSERT_EQ(original.status().code(), MetaServiceCode::OK);
+ ASSERT_TRUE(original.is_lazy_commit_incomplete());
+ check_status(TxnStatusPB::TXN_STATUS_COMMITTED);
+ auto before = business_metadata();
+ auto check_incomplete_retry = [&](const CommitTxnResponse& retry) {
+ ASSERT_EQ(retry.status().code(), MetaServiceCode::OK) <<
retry.ShortDebugString();
+ EXPECT_EQ(retry.txn_info().txn_id(), request_.txn_id());
+ EXPECT_EQ(retry.txn_info().status(), TxnStatusPB::TXN_STATUS_VISIBLE);
+ ASSERT_EQ(retry.txn_info().table_ids_size(), 1);
+ EXPECT_EQ(retry.txn_info().table_ids(0), TABLE_ID);
+ // FE uses both flags to recognize incomplete publication, including
+ // when this request originally selected the immediate handler.
+ EXPECT_TRUE(retry.is_lazy_commit());
+ EXPECT_TRUE(retry.is_lazy_commit_incomplete());
+ EXPECT_TRUE(retry.partition_ids().empty());
+ EXPECT_TRUE(retry.table_ids().empty());
+ EXPECT_TRUE(retry.versions().empty());
+ EXPECT_TRUE(retry.table_stats().empty());
+ EXPECT_EQ(business_metadata(), before);
+ check_status(TxnStatusPB::TXN_STATUS_COMMITTED);
+ };
+
+ // All tmp rowsets have gone, so this retry selects immediate.
+ request_.set_enable_txn_lazy_commit(false);
+ auto immediate_retry = commit(request_);
+ check_incomplete_retry(immediate_retry);
+
+ // Force the other entry point despite the empty tmp scan. It must return
+ // the same committed-but-incomplete response when its lazy task fails.
+ config::cloud_txn_lazy_commit_fuzzy_possibility = 100;
+ request_.set_enable_txn_lazy_commit(true);
+ auto lazy_retry = commit(request_);
+ check_incomplete_retry(lazy_retry);
+}
+
TEST(TxnLazyCommitTest, CommitTxnEventuallyWithFailedLazyCommitTaskTest) {
auto txn_kv = get_mem_txn_kv();
@@ -1696,6 +2047,14 @@ TEST(TxnLazyCommitTest,
FallThroughCommitTxnEventuallyTest) {
ASSERT_EQ(res.status().code(), MetaServiceCode::OK) <<
res.ShortDebugString();
ASSERT_TRUE(commit_txn_immediatelly_hit);
ASSERT_TRUE(commit_txn_eventually_finish_hit);
+ // The failed immediate attempt already populated partition versions.
+ // Falling back must return only the successful lazy attempt's results.
+ ASSERT_EQ(res.partition_ids_size(), 1);
+ ASSERT_EQ(res.table_ids_size(), 1);
+ ASSERT_EQ(res.versions_size(), 1);
+ EXPECT_EQ(res.partition_ids(0), partition_id);
+ EXPECT_EQ(res.table_ids(0), table_id);
+ EXPECT_EQ(res.versions(0), 2);
}
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]