This is an automated email from the ASF dual-hosted git repository.
Yukang-Lian 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 0ba9b361c79 [fix](cloud) let a restarted schema change replace its
stale STOP_TOKEN (#68398)
0ba9b361c79 is described below
commit 0ba9b361c79d0bf0bd2cb826e5786ed7e8c06b7f
Author: Jamie <[email protected]>
AuthorDate: Thu Oct 22 14:53:42 2026 +0800
[fix](cloud) let a restarted schema change replace its stale STOP_TOKEN
(#68398)
### What problem does this PR solve?
Related PR: #48399 (introduced the schema change `STOP_TOKEN`), #49275
(unregisters the token when `commit_tablet_job` fails), #66082 (a load
must not preempt the schema change delete bitmap lock)
Problem Summary:
A heavy schema change on a cloud MOW table gets CANCELLED when its BE
restarts while the delete bitmap is being recalculated, and loads on the
table fail with delete bitmap lock conflicts for a while afterwards.
`CloudSchemaChangeJob::_process_delete_bitmap` registers a compaction
`STOP_TOKEN` on the new tablet before it recalculates the delete bitmap.
When the BE dies at that point, nothing unregisters the token: it stays
in the meta service until its lease expires
(`lease_compaction_interval_seconds * 4` = 80s), and the dead run's
initiator stays in the table's schema change delete bitmap lock
(`lock_id=-2`).
FE re-sends the ALTER task a couple of seconds after the BE comes back.
The schema change job record survives that restart on purpose
(`start_schema_change_job` takes the "same job restarts" path because
the job id and the BE-address initiator are unchanged), but the new run
uses a fresh `delete_bitmap_lock_initiator`, and `start_compaction_job`
rejects its `STOP_TOKEN` because the stale one is still there:
```
failed to start tablet job: compactions are not allowed on tablet_id=...
currently, blocked by schema change job delete_bitmap_initiator=...
```
The BE reports that as `INTERNAL_ERROR`, which
`AlterJobV2.getRetryTimes` does not retry, so FE cancels the whole
schema change job. The new run also cannot release the stale lock
(`failed to remove delete bitmap update lock: lock initiator not
exist`), so until it expires (`delete_bitmap_lock_expiration_seconds`)
loads fail with `Failed to get delete bitmap lock due to conflict`: a
load may force-take a load/compaction lock but never a schema change
lock (#66082).
The same check ordering also breaks idempotency: a retried `STOP_TOKEN`
registration of the same run hits the `STOP_TOKEN` conflict check before
the same-id check and gets `JOB_TABLET_BUSY`.
### Changes
`start_compaction_job` no longer rejects an incoming `STOP_TOKEN`
because of an existing `STOP_TOKEN`. The incoming token replaces any
stale token whose `delete_bitmap_lock_initiator` differs, and the schema
change delete bitmap lock held by that stale initiator is released in
the same transaction (`remove_delete_bitmap_update_lock` with
`SCHEMA_CHANGE_DELETE_BITMAP_LOCK_ID`, respecting the instance's v1/v2
lock version, which `start_tablet_job` now passes in). FE guarantees at
most one schema change job per tablet, so the incoming token always
belongs to the live run. Regular compactions are still blocked by a live
token, and the BE/FE sides are unchanged.
New unit test `MetaServiceJobTest.StopTokenReplacesStaleStopToken`: a
stale token plus a `-2` lock held by the dead initiator; a regular
compaction is still rejected; the new token is accepted and the stale
token and lock are gone; re-registering the same token is idempotent;
the live token still blocks compactions.
### Testing
- `./run-cloud-ut.sh --run
--filter='meta_service_test:MetaServiceJobTest.*'` passes, including the
new test.
- Reproduced and verified end to end on a single FE/BE cloud cluster
with a debug point that blocks the schema change right after it acquires
the delete bitmap lock, then restarting the BE: without this change the
re-sent ALTER task is rejected and the job is CANCELLED every time; with
it the meta service logs `replace stale STOP_TOKEN ...` followed by
`remove delete bitmap lock ... lock_id=-2 initiator=<stale>`, the same
job finishes within a few seconds of the restart, and loads on the table
are not blocked.
### Release note
Cloud MOW: a heavy schema change no longer gets CANCELLED when its BE
restarts while the delete bitmap is being recalculated, and loads on the
table are no longer blocked by the dead run's schema change lock.
---
cloud/src/meta-service/meta_service_job.cpp | 44 +++++++--
cloud/test/meta_service_job_test.cpp | 134 ++++++++++++++++++++++++++++
2 files changed, 169 insertions(+), 9 deletions(-)
diff --git a/cloud/src/meta-service/meta_service_job.cpp
b/cloud/src/meta-service/meta_service_job.cpp
index 6818968c821..34dda7ec920 100644
--- a/cloud/src/meta-service/meta_service_job.cpp
+++ b/cloud/src/meta-service/meta_service_job.cpp
@@ -146,11 +146,16 @@ static inline bool
may_conflict_by_type(TabletCompactionJobPB::CompactionType a,
return is_rowset_compaction_family(a) && is_rowset_compaction_family(b);
}
+static void remove_delete_bitmap_update_lock(std::unique_ptr<Transaction>& txn,
+ const std::string& instance_id,
int64_t table_id,
+ int64_t tablet_id, int64_t
lock_id,
+ int64_t lock_initiator,
std::string use_version);
+
void start_compaction_job(MetaServiceCode& code, std::string& msg,
std::stringstream& ss,
std::unique_ptr<Transaction>& txn, const
StartTabletJobRequest* request,
StartTabletJobResponse* response, std::string&
instance_id,
- bool& need_commit, bool is_versioned_read,
- ResourceManager* resource_mgr) {
+ bool& need_commit, bool is_versioned_read,
ResourceManager* resource_mgr,
+ const std::string& use_version) {
auto& compaction = request->job().compaction(0);
if (!compaction.has_id() || compaction.id().empty()) {
code = MetaServiceCode::INVALID_ARGUMENT;
@@ -274,12 +279,12 @@ void start_compaction_job(MetaServiceCode& code,
std::string& msg, std::stringst
}), compactions.end());
// clang-format on
// Check conflict job
- if (std::ranges::any_of(compactions, [](const auto& c) {
- return c.type() == TabletCompactionJobPB::STOP_TOKEN;
- })) {
- auto it = std::ranges::find_if(compactions, [](const auto& c) {
- return c.type() == TabletCompactionJobPB::STOP_TOKEN;
- });
+ auto is_stop_token = [](const auto& c) {
+ return c.type() == TabletCompactionJobPB::STOP_TOKEN;
+ };
+ if (compaction.type() != TabletCompactionJobPB::STOP_TOKEN &&
+ std::ranges::any_of(compactions, is_stop_token)) {
+ auto it = std::ranges::find_if(compactions, is_stop_token);
msg = fmt::format(
"compactions are not allowed on tablet_id={} currently,
blocked by schema "
"change job delete_bitmap_initiator={}",
@@ -293,6 +298,25 @@ void start_compaction_job(MetaServiceCode& code,
std::string& msg, std::stringst
// compaction operations and canceling other types of compaction.
compactions.Clear();
} else if (compaction.type() == TabletCompactionJobPB::STOP_TOKEN) {
+ // A STOP_TOKEN outlives the schema change run that registered it
when its BE dies
+ // before unregistering it: nothing renews or removes the token
until its lease
+ // expires (lease_compaction_interval_seconds * 4). FE guarantees
at most one schema
+ // change job per tablet and every run uses a fresh
delete_bitmap_lock_initiator, so
+ // an incoming STOP_TOKEN always belongs to the live run. Replace
the stale token and
+ // release the schema change delete bitmap lock its dead owner
still holds; otherwise
+ // the re-sent ALTER task is rejected here and FE cancels the
whole job.
+ for (const auto& c : compactions) {
+ if (!is_stop_token(c) || !c.has_delete_bitmap_lock_initiator()
||
+ c.delete_bitmap_lock_initiator() ==
compaction.delete_bitmap_lock_initiator()) {
+ continue;
+ }
+ INSTANCE_LOG(INFO)
+ << "replace stale STOP_TOKEN, tablet_id=" << tablet_id
+ << " new_job_id=" << compaction.id() << " stale_job="
<< proto_to_json(c);
+ remove_delete_bitmap_update_lock(txn, instance_id, table_id,
tablet_id,
+
SCHEMA_CHANGE_DELETE_BITMAP_LOCK_ID,
+
c.delete_bitmap_lock_initiator(), use_version);
+ }
// fail all existing compactions
compactions.Clear();
} else if ((!compaction.has_check_input_versions_range() &&
@@ -604,8 +628,10 @@ void
MetaServiceImpl::start_tablet_job(::google::protobuf::RpcController* contro
};
if (!request->job().compaction().empty()) {
+ std::string use_version =
+
delete_bitmap_lock_white_list_->get_delete_bitmap_lock_version(instance_id);
start_compaction_job(code, msg, ss, txn, request, response,
instance_id, need_commit,
- is_versioned_read, resource_mgr_.get());
+ is_versioned_read, resource_mgr_.get(),
use_version);
return;
}
diff --git a/cloud/test/meta_service_job_test.cpp
b/cloud/test/meta_service_job_test.cpp
index 2e184c2ee9d..87a07325294 100644
--- a/cloud/test/meta_service_job_test.cpp
+++ b/cloud/test/meta_service_job_test.cpp
@@ -1788,6 +1788,140 @@ TEST(MetaServiceJobTest,
StopTokenSkipsStaleTabletCacheCheck) {
}
}
+// A STOP_TOKEN left behind by a schema change run whose BE
+// died before unregistering it stays valid until its lease expires. The
re-sent ALTER task
+// registers a new STOP_TOKEN with a fresh delete_bitmap_lock_initiator; it
must replace the
+// stale token and release the schema change delete bitmap lock the dead run
still holds,
+// instead of being rejected with JOB_TABLET_BUSY (which makes FE cancel the
whole job).
+TEST(MetaServiceJobTest, StopTokenReplacesStaleStopToken) {
+ auto meta_service = get_meta_service();
+
+ auto sp = SyncPoint::get_instance();
+ DORIS_CLOUD_DEFER {
+ SyncPoint::get_instance()->clear_all_call_backs();
+ };
+ sp->set_call_back("get_instance_id", [&](auto&& args) {
+ auto* ret = try_any_cast_ret<std::string>(args);
+ ret->first = instance_id;
+ ret->second = true;
+ });
+ sp->enable_processing();
+
+ int64_t table_id = 1, index_id = 2, partition_id = 3, tablet_id = 102;
+ constexpr int64_t stale_initiator = 111;
+ constexpr int64_t new_initiator = 222;
+
+ {
+ auto index_key = meta_tablet_idx_key({instance_id, tablet_id});
+ TabletIndexPB idx_pb;
+ idx_pb.set_table_id(table_id);
+ idx_pb.set_index_id(index_id);
+ idx_pb.set_partition_id(partition_id);
+ idx_pb.set_tablet_id(tablet_id);
+ std::string stats_key =
+ stats_tablet_key({instance_id, table_id, index_id,
partition_id, tablet_id});
+ TabletStatsPB stats;
+ stats.set_base_compaction_cnt(0);
+ stats.set_cumulative_compaction_cnt(0);
+ std::unique_ptr<Transaction> txn;
+ ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn),
TxnErrorCode::TXN_OK);
+ txn->put(index_key, idx_pb.SerializeAsString());
+ txn->put(stats_key, stats.SerializeAsString());
+ ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+ }
+
+ auto register_stop_token = [&](const std::string& job_id, int64_t
initiator,
+ StartTabletJobResponse& res) {
+ brpc::Controller cntl;
+ StartTabletJobRequest req;
+ req.mutable_job()->mutable_idx()->set_tablet_id(tablet_id);
+ auto* compaction = req.mutable_job()->add_compaction();
+ compaction->set_id(job_id);
+ compaction->set_initiator("ip:port");
+ compaction->set_type(TabletCompactionJobPB::STOP_TOKEN);
+ compaction->set_base_compaction_cnt(0);
+ compaction->set_cumulative_compaction_cnt(0);
+ long now = time(nullptr);
+ compaction->set_expiration(now + 12);
+ compaction->set_lease(now + 80);
+ compaction->set_delete_bitmap_lock_initiator(initiator);
+ meta_service->start_tablet_job(&cntl, &req, &res, nullptr);
+ };
+ auto read_tablet_job = [&]() {
+ TabletJobInfoPB job_pb;
+ std::unique_ptr<Transaction> txn;
+ EXPECT_EQ(meta_service->txn_kv()->create_txn(&txn),
TxnErrorCode::TXN_OK);
+ std::string job_val;
+ EXPECT_EQ(
+ txn->get(job_tablet_key({instance_id, table_id, index_id,
partition_id, tablet_id}),
+ &job_val),
+ TxnErrorCode::TXN_OK);
+ EXPECT_TRUE(job_pb.ParseFromString(job_val));
+ return job_pb;
+ };
+
+ // The dead run registered its STOP_TOKEN and still holds the schema
change lock.
+ {
+ StartTabletJobResponse res;
+ register_stop_token("stale_stop_token", stale_initiator, res);
+ ASSERT_EQ(res.status().code(), MetaServiceCode::OK) <<
res.status().msg();
+ }
+ {
+ DeleteBitmapUpdateLockPB lock_info;
+ lock_info.set_lock_id(SCHEMA_CHANGE_DELETE_BITMAP_LOCK_ID);
+ lock_info.set_expiration(time(nullptr) + 60);
+ lock_info.add_initiators(stale_initiator);
+ std::unique_ptr<Transaction> txn;
+ ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn),
TxnErrorCode::TXN_OK);
+ txn->put(meta_delete_bitmap_update_lock_key({instance_id, table_id,
-1}),
+ lock_info.SerializeAsString());
+ ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK);
+ }
+
+ // A regular compaction is still blocked by the unexpired STOP_TOKEN.
+ {
+ StartTabletJobResponse res;
+ start_compaction_job(meta_service.get(), tablet_id, "cumu_job_1",
"ip:port",
+ /*base_cnt=*/0, /*cumu_cnt=*/0,
TabletCompactionJobPB::CUMULATIVE,
+ res);
+ ASSERT_EQ(res.status().code(), MetaServiceCode::JOB_TABLET_BUSY);
+ }
+
+ // The re-sent ALTER task registers a new STOP_TOKEN: it replaces the
stale one and the
+ // dead run's schema change lock is released.
+ {
+ StartTabletJobResponse res;
+ register_stop_token("new_stop_token", new_initiator, res);
+ ASSERT_EQ(res.status().code(), MetaServiceCode::OK)
+ << "new STOP_TOKEN must replace the stale one; got: " <<
res.status().msg();
+ auto job_pb = read_tablet_job();
+ ASSERT_EQ(job_pb.compaction_size(), 1);
+ EXPECT_EQ(job_pb.compaction(0).id(), "new_stop_token");
+ EXPECT_EQ(job_pb.compaction(0).delete_bitmap_lock_initiator(),
new_initiator);
+ check_delete_bitmap_lock(meta_service.get(), instance_id, table_id,
+ SCHEMA_CHANGE_DELETE_BITMAP_LOCK_ID,
/*exist=*/false);
+ }
+
+ // Re-registering the same STOP_TOKEN (RPC retry) is idempotent.
+ {
+ StartTabletJobResponse res;
+ register_stop_token("new_stop_token", new_initiator, res);
+ ASSERT_EQ(res.status().code(), MetaServiceCode::OK) <<
res.status().msg();
+ auto job_pb = read_tablet_job();
+ ASSERT_EQ(job_pb.compaction_size(), 1);
+ EXPECT_EQ(job_pb.compaction(0).id(), "new_stop_token");
+ }
+
+ // The live token keeps blocking regular compactions.
+ {
+ StartTabletJobResponse res;
+ start_compaction_job(meta_service.get(), tablet_id, "cumu_job_2",
"ip:port",
+ /*base_cnt=*/0, /*cumu_cnt=*/0,
TabletCompactionJobPB::CUMULATIVE,
+ res);
+ ASSERT_EQ(res.status().code(), MetaServiceCode::JOB_TABLET_BUSY);
+ }
+}
+
TEST(MetaServiceJobTest, DeleteBitmapUpdateLockCompatibilityTest) {
auto meta_service = get_meta_service();
auto sp = SyncPoint::get_instance();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]