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 e4d328e23b8 [fix](mow) Skip delete bitmap cache prefill for
non-running tablets (#68436)
e4d328e23b8 is described below
commit e4d328e23b888ea9698f7e58c99dcf460580e122
Author: bobhan1 <[email protected]>
AuthorDate: Thu Sep 24 14:51:49 2026 +0800
[fix](mow) Skip delete bitmap cache prefill for non-running tablets (#68436)
### What problem does this PR solve?
Cloud schema change can allow cumulative compaction on a
`TABLET_NOTREADY` tablet before its delete bitmap is complete.
Post-compaction prefill then caches empty or incomplete aggregate
bitmaps at the tablet's maximum version. Schema change later installs
the completed bitmap and marks the tablet `TABLET_RUNNING`, but the
cache keys are unchanged. Queries can reuse the stale bitmaps and return
overwritten rows.
Require `TABLET_RUNNING` under the existing tablet header read lock
before either post-compaction prefill mode runs. Normal prefill on
running tablets remains enabled.
Add a parameterized BE test that keeps the same rowsets and read version
while installing the schema-change bitmap and transitioning the tablet
to RUNNING. It covers all four prefill switch combinations, output and
non-output rowsets, and non-contiguous segment IDs. Each test instance
uses unique rowset IDs to isolate the global aggregate cache.
### Release note
Fix duplicate rows returned by Unique Key Merge-on-Write queries when
schema change overlaps compaction.
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- [ ] Manual test
- [ ] No need to test or manual test.
Validation:
- Before the fix: three combinations with prefill enabled fail with
stale empty bitmaps; disabling both switches passes.
- After the fix: 56 ASAN BE unit tests pass, including all four new
parameter combinations, `CloudTabletDeleteBitmapTest`, `TabletMetaTest`,
and `CloudCompactionTest`.
- Command: `./run-be-ut.sh --run
--filter='PrefillModes/CloudTabletDeleteBitmapPrefillTest.*:CloudTabletDeleteBitmapTest.*:TabletMetaTest.*:CloudCompactionTest.*'
-j100`
- Changed C++ files pass clang-format 16, build hygiene checks, and `git
diff --check`.
- The existing Docker regression cases
`test_schema_change_with_compaction9` and
`test_schema_change_with_compaction10` cover the original interleaving
but were not run locally. Final-change clang-tidy was not run.
- Behavior changed:
- [ ] No.
- [x] Yes. Post-compaction aggregate delete bitmap cache prefill is
skipped until the tablet is RUNNING.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/storage/tablet/base_tablet.cpp | 5 +++
be/test/cloud/cloud_tablet_test.cpp | 78 +++++++++++++++++++++++++++++++++++
2 files changed, 83 insertions(+)
diff --git a/be/src/storage/tablet/base_tablet.cpp
b/be/src/storage/tablet/base_tablet.cpp
index c7ad7a6c639..71a5cd6c641 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -2390,6 +2390,11 @@ void
BaseTablet::prefill_dbm_agg_cache_after_compaction(const RowsetSharedPtr& o
int64_t cur_max_version {-1};
{
std::shared_lock rlock(get_header_lock());
+ // Schema change may still be rebuilding the delete bitmap of a
NOTREADY tablet.
+ // Prefilling now can cache incomplete bitmaps that remain stale
after it becomes RUNNING.
+ if (tablet_state() != TABLET_RUNNING) {
+ return;
+ }
cur_max_version = max_version_unlocked();
}
if (config::enable_prefill_all_dbm_agg_cache_after_compaction) {
diff --git a/be/test/cloud/cloud_tablet_test.cpp
b/be/test/cloud/cloud_tablet_test.cpp
index 0480c0a2510..3707815c268 100644
--- a/be/test/cloud/cloud_tablet_test.cpp
+++ b/be/test/cloud/cloud_tablet_test.cpp
@@ -26,6 +26,7 @@
#include <cstdint>
#include <future>
#include <mutex>
+#include <tuple>
#include "cloud/cloud_meta_mgr.h"
#include "cloud/cloud_storage_engine.h"
@@ -35,7 +36,9 @@
#include "storage/rowset/rowset.h"
#include "storage/rowset/rowset_factory.h"
#include "storage/rowset/rowset_meta.h"
+#include "storage/rowset/unique_rowset_id_generator.h"
#include "storage/tablet/tablet_meta.h"
+#include "util/defer_op.h"
#include "util/uid_util.h"
namespace doris {
@@ -149,6 +152,81 @@ TEST_F(CloudTabletDeleteBitmapTest,
AggDeleteBitmapForCompactionReturnsPreRowset
EXPECT_EQ(rowset_versions_without_stats, pre_rowset_to_versions);
}
+class CloudTabletDeleteBitmapPrefillTest
+ : public CloudTabletWarmUpStateTest,
+ public testing::WithParamInterface<std::tuple<bool, bool>> {
+public:
+ void SetUp() override {
+ CloudTabletWarmUpStateTest::SetUp();
+ // The aggregate cache is global, so rowset IDs must be unique across
test instances.
+ _engine._rowset_id_generator =
+ std::make_unique<UniqueRowsetIdGenerator>(UniqueId::gen_uid());
+ }
+};
+
+TEST_P(CloudTabletDeleteBitmapPrefillTest, WaitForSchemaChangeDeleteBitmap) {
+ const auto old_prefill_output =
config::enable_prefill_output_dbm_agg_cache_after_compaction;
+ const auto old_prefill_all =
config::enable_prefill_all_dbm_agg_cache_after_compaction;
+ Defer restore_config {[&] {
+ config::enable_prefill_output_dbm_agg_cache_after_compaction =
old_prefill_output;
+ config::enable_prefill_all_dbm_agg_cache_after_compaction =
old_prefill_all;
+ }};
+ const auto [prefill_output, prefill_all] = GetParam();
+ config::enable_prefill_output_dbm_agg_cache_after_compaction =
prefill_output;
+ config::enable_prefill_all_dbm_agg_cache_after_compaction = prefill_all;
+
+ _tablet->tablet_meta()->set_enable_unique_key_merge_on_write(true);
+ _tablet->tablet_meta()->mutable_tablet_schema()->_keys_type = UNIQUE_KEYS;
+ auto output_rowset = create_rowset(Version(2, 3), 2);
+ auto other_rowset = create_rowset(Version(4, 4), 2);
+ ASSERT_NE(output_rowset, nullptr);
+ ASSERT_NE(other_rowset, nullptr);
+ output_rowset->rowset_meta()->set_segment_ids({3, 7});
+ other_rowset->rowset_meta()->set_segment_ids({5, 9});
+ const std::vector<RowsetSharedPtr> rowsets {output_rowset, other_rowset};
+ {
+ std::unique_lock lock(_tablet->get_header_lock());
+ ASSERT_TRUE(_tablet->set_tablet_state(TABLET_NOTREADY).ok());
+ _tablet->add_rowsets(rowsets, false, lock, false);
+ }
+
+ // Compaction finishes before schema change installs the complete delete
bitmap.
+ _tablet->prefill_dbm_agg_cache_after_compaction(output_rowset);
+ auto cache_before =
DeleteBitmapAggCache::instance()->snapshot(_tablet->tablet_id());
+ DeleteBitmap complete_delete_bitmap(_tablet->tablet_id());
+ for (const auto& rowset : rowsets) {
+ for (auto segment : rowset->segments()) {
+ auto key = segment.delete_bitmap_key(4);
+ EXPECT_EQ(cache_before.get(key), nullptr);
+ complete_delete_bitmap.add(key, 1);
+ }
+ }
+ {
+ std::unique_lock lock(_tablet->get_header_lock());
+ _tablet->tablet_meta()->delete_bitmap() = complete_delete_bitmap;
+ ASSERT_TRUE(_tablet->set_tablet_state(TABLET_RUNNING).ok());
+ }
+
+ // Prefill must still work for RUNNING tablets, honoring both
configuration switches.
+ _tablet->prefill_dbm_agg_cache_after_compaction(output_rowset);
+ auto cache_after =
DeleteBitmapAggCache::instance()->snapshot(_tablet->tablet_id());
+ for (const auto& rowset : rowsets) {
+ const bool should_prefill = prefill_all || (prefill_output && rowset
== output_rowset);
+ for (auto segment : rowset->segments()) {
+ auto key = segment.delete_bitmap_key(4);
+ EXPECT_EQ(cache_after.get(key) != nullptr, should_prefill);
+ // The read version and rowset IDs are unchanged after schema
change. A premature
+ // prefill would return the old empty bitmap and expose
overwritten rows here.
+ auto bitmap = _tablet->tablet_meta()->delete_bitmap().get_agg(key);
+ EXPECT_EQ(bitmap->cardinality(), 1);
+ EXPECT_TRUE(bitmap->contains(1));
+ }
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(PrefillModes, CloudTabletDeleteBitmapPrefillTest,
+ testing::Combine(testing::Bool(), testing::Bool()));
+
// Test get_rowset_warmup_state for non-existent rowset
TEST_F(CloudTabletWarmUpStateTest, TestGetRowsetWarmupStateNonExistent) {
auto rowset = create_rowset(Version(1, 1));
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]