Fix LOGICAL_ERROR "Part X intersects previous part Y" during ReplicatedMergeTree startup#103537
Conversation
…atedMergeTree startup `StorageReplicatedMergeTree::checkPartsImpl` builds a covering set of empty unexpected parts (`set_of_empty_unexpected_parts`) by adding every empty unexpected part whose `uncovered` flag is `true`. The `uncovered` flag is set in `MergeTreeData::loadDataParts` to `true` when no other unexpected part's `MergeTreePartInfo` *contains* the current one. Two empty unexpected parts on disk can have intersecting block ranges without one fully containing the other (for example, `all_0_5_2` and `all_2_11_3`). Both parts pass the `uncovered = true` check, but `ActiveDataPartSet::add` calls `addImpl`, which throws `LOGICAL_ERROR` with the "Part X intersects previous part Y" message when the second part is added. The exception is thrown on the `BackgroundSchedulePool` thread running `ReplicatedMergeTreeAttachThread::runImpl`, aborting startup of the table. Replace `add` with `tryAdd` and log a warning when the conflicting empty marker is skipped. The set is only consulted later as a heuristic — when an unexpected part is matched against `getContainingPart` to decide whether it looks like a leftover of a dropped part — so keeping just one of two intersecting empty markers is sufficient. Any unexpected part that would have been covered by the skipped marker simply falls through to the regular handling path that detaches it as `ignored` further down in the same loop. Add a small `gtest_active_data_part_set` unit test that documents the contract: `add` throws on intersection without containment while `tryAdd` returns `HasIntersectingPart` without throwing. Reproduction comes from the BuzzHouse fuzzer (STID `2352-49be`): https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=100943&sha=3308ce056bdfd5672c3c300ccae03968beac9846&name_0=PR&name_1=BuzzHouse%20%28arm_asan_ubsan%29 Original PR with the BuzzHouse finding: ClickHouse#100943
|
Pre-PR validation gate (cron task-worker session
Local builds: |
|
Workflow [PR], commit [6b1f736] Summary: ✅ AI ReviewSummaryThis PR fixes a real startup exception path in ClickHouse Rules
Final Verdict
|
| /// covering candidate is sufficient — any unexpected part that the skipped marker | ||
| /// would have covered will fall through to the regular handling path below. | ||
| String reason; | ||
| if (set_of_empty_unexpected_parts.tryAdd(load_state.part->name, &reason) |
There was a problem hiding this comment.
Switching from add to tryAdd avoids the immediate LOGICAL_ERROR, but skipping one intersecting empty marker can still change startup behavior in a harmful way.
Concrete case:
- markers:
all_0_5_2andall_2_11_3(intersect, neither contains the other), - unexpected part:
all_9_10_1.
If all_2_11_3 is the one skipped, getContainingPart(all_9_10_1) no longer finds an empty covering marker, so this part is reclassified as uncovered and contributes to uncovered_unexpected_parts_rows, which can still trip the later sanity-check exception (TOO_MANY_UNEXPECTED_DATA_PARTS) during startup.
So the intersecting markers are not always interchangeable as covering candidates for downstream checks.
…ates The first round used `ActiveDataPartSet::tryAdd` and silently skipped the second of two intersecting empty markers. Bot review on PR [ClickHouse#103537](ClickHouse#103537) flagged a downstream regression: an unexpected non-empty part covered ONLY by the skipped marker is reclassified as uncovered and contributes to `uncovered_unexpected_parts_rows`, which can still trip the later `TOO_MANY_UNEXPECTED_DATA_PARTS` exception during startup. Concrete failure mode (from the bot's example): - markers: `all_0_5_2` (blocks 0..5) and `all_2_11_3` (blocks 2..11) - unexpected part: `all_9_10_1` (blocks 9..10) `all_9_10_1` is contained ONLY by `all_2_11_3`. If `all_2_11_3` is the marker that lands in the auxiliary list, `getContainingPart(all_9_10_1)` must still find it — otherwise startup that previously failed with `LOGICAL_ERROR` now fails with `TOO_MANY_UNEXPECTED_DATA_PARTS` instead. Fix: introduce `OverlappingPartCovering` (header-only). It holds a primary `ActiveDataPartSet` for fast lookup of non-overlapping markers and a small auxiliary list for the rest. `getContainingPart` consults both, so coverage detection is order-independent and every marker contributes to coverage regardless of the order in which it was added. Replaces the inline `tryAdd` in `StorageReplicatedMergeTree::checkPartsImpl` with this helper. Unit tests in `gtest_active_data_part_set.cpp` extended with the bot's exact scenario (`all_0_5_2` + `all_2_11_3` covering `all_9_10_1`) in both insertion orders, plus mixed-overlap, disjoint, contained, and empty-state cases. The existing `ActiveDataPartSet` contract tests are preserved.
|
@clickhouse-gh — thanks for the catch. Pushed The original Reworked fix: new header-only helper Walking through your concrete example with the new helper:
Unit tests in Pre-PR validation gate (rework):
Build + test:
Session: cron:clickhouse-ci-task-worker:20260426-224500 |
LLVM Coverage Report
Changed lines: 99.24% (130/131) | lost baseline coverage: 25 line(s) · Uncovered code |

Fix a rare
LOGICAL_ERROR"Part X intersects previous part Y" that abortsReplicatedMergeTreestartup when two empty unexpected parts on disk haveintersecting block ranges without one fully containing the other.
Failure
Stack trace:
Hit by the BuzzHouse fuzzer twice in 76 days (rare chronic trunk bug):
arm_asan_ubsan): https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=100943&sha=3308ce056bdfd5672c3c300ccae03968beac9846&name_0=PR&name_1=BuzzHouse%20%28arm_asan_ubsan%29amd_binary, ParallelReplicas, s3 storage, sequential)Maintainer directive to file a separate fix PR for this finding:
#100943 (comment)
Root cause
StorageReplicatedMergeTree::checkPartsImplbuilds a covering set of emptyunexpected parts:
The
uncoveredflag is computed inMergeTreeData::loadDataParts(
MergeTreeData.cpp:2479) as: "no other unexpected part'sMergeTreePartInfocontains this one". Containment is strict — a partcan be unexpected, uncovered, and still intersect another uncovered
unexpected part:
all_0_5_2: blocks 0..5, level 2all_2_11_3: blocks 2..11, level 3Neither contains the other (
minandmaxblock numbers don't satisfythe containment relation), but they intersect at blocks 2..5. Both pass
the
uncovered == truefilter;ActiveDataPartSet::addrejects thesecond one with
LOGICAL_ERRORand aborts the table-startup attachthread. This typically arises in fuzzer workloads (concurrent
DROP/merges/INSERT) that leave overlapping empty drop-marker partson disk.
Fix
Use
ActiveDataPartSet::tryAddand log a warning instead of throwing.The set is only consulted as a heuristic —
getContainingPartagainstunexpected parts that look like leftovers of dropped ranges. If two
empty markers intersect, either one is an equally valid covering
candidate; any unexpected part that the skipped marker would have
covered falls through to the existing handling path further down in
the same loop, where every entry of
unexpected_data_partsisultimately renamed to
detached/ignoredafter the sanity check.A unit test (
gtest_active_data_part_set) is added to document thecontract:
addthrows on intersection without containment whiletryAddreturnsHasIntersectingPartwithout throwing.Relation to PR #100992
PR #100992 by @tuanpach addresses the analogous "Part intersects" issue
in
MergeTreeData::PartLoadingTree::add— a different code path(
loadDataPartsrather than the replication-startupcheckPartsImpl).That fix builds a transaction-aware enum (
RollbackStatus) to decidewhich intersecting transactional part to keep. That sophistication is
appropriate for
PartLoadingTree, which feeds the livedata_partsset; the empty unexpected parts examined here, by contrast, are already
non-transactional drop markers slated to be detached as
ignored, sochoosing arbitrarily between two intersecting candidates is sound.
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed a rare
LOGICAL_ERROR"Part X intersects previous part Y" raised duringReplicatedMergeTreetable startup when two empty unexpected parts on disk had overlapping but non-containing block ranges. The exception aborted the table-attach thread and prevented the table from coming up.Documentation entry for user-facing changes
Version info
26.5.1.204