Reclaim stale empty-part directory instead of LOGICAL_ERROR#108879
Conversation
A covering empty part (DROP/DETACH/MOVE/REPLACE PARTITION on a non-replicated MergeTree) is created by createEmptyPart() under a tmp_empty_<part> directory and later renamed to its persistent name. When the operation is interrupted after the part reaches PreActive with a deferred rename and then rolls back (for example a fault-injected metadata commit on plain_rewritable object storage, or a crash), the tmp_empty_<part> directory is left on disk because the rolled-back part is no longer is_temp and its physical directory is cleaned only later by the temporary-directory sweeper. A subsequent covering operation on the same source part derives the same name and createEmptyPart() aborted the server with a LOGICAL_ERROR "New empty part is about to materialize but the directory already exist". The in-memory temporary-parts holder acquired just above (getTemporaryPartDirectoryHolder) already guarantees no concurrent operation owns this name, so an existing directory can only be a stale leftover and is safe to remove. Reclaim it (log a warning and removeRecursive) before beginTransaction(), mirroring the regular INSERT path in MergeTreeDataWriter which already reclaims a stale temporary directory instead of failing. Adds a regression test using a new failpoint that injects the stale directory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-0:20260630-021400 |
|
cc @CurtizJ @alesapin — could you review this? It fixes a |
|
Workflow [PR], commit [dec95c9] Summary: ✅
AI ReviewSummaryThis PR makes Final Verdict
|
CI finish ledger — 90792cdEvery failure below has an owner: a fixing PR (ours or external). Only
Session id: cron:our-pr-ci-monitor:20260630-073000 |
|
@groeneai merge with master |
|
@groeneai, merge with master |
|
Merged master @ 2ed2e36. Rebuilt clean; the fix (4 files: FailPoint.cpp, MergeTreeData.cpp, test) is unchanged. |
…y-part-stale-tmp-dir
2ed2e36 to
ff76af0
Compare
Both MutateTask empty-part callers (TTL-drop and delete-only) discarded the scope_guard returned by createEmptyPart, so the physical tmp_empty_<part> directory could outlive its in-memory temporary_parts entry until the later rename/commit. That left a window where a concurrent operation re-entering the same empty-part name would see no temporary_parts owner and the stale-directory reclaim in createEmptyPart could delete a live temp directory of an in-flight mutation. Store the holder in MutationContext::temporary_directory_lock (the member the normal mutation path already uses) so temporary_parts stays authoritative for every createEmptyPart caller and the reclaim only ever removes a provably-stale leftover. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI finish ledger — 350a696CI fully finished on this head (Finish Workflow pass, Mergeable Check pass, PR aggregator pass). No test failures. The earlier arm_tidy duplicate-include failure was on head
Session id: cron:our-pr-ci-monitor:20260713-093000 |
In replacePartWithEmpty, drop the wrapped mutation's old part (living under tmp_mut_<part>) while its directory holder in ctx->temporary_directory_lock is still alive, and only then install the new tmp_empty_<part> holder. Previously the new holder was installed first, releasing the tmp_mut_<part> holder before the old part was dropped, which left a window where the physical tmp_mut_<part> directory had no temporary_parts entry (a lock-before-cleanup violation flagged in code review). The local tmp_dir_holder keeps tmp_empty_<part> registered across the reorder, so both directories stay protected at all times. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@alexey-milovidov merged master @ 5aa0f8d (base 7b7de46). Also addressed the follow-up bot review on the TTL-to-empty path: in |
|
@groeneai, merge with master |
|
@hanfei1991 Merged current master and pushed @ dec95c9. Branch was 5 commits behind; now up to date. CI re-running. |
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 36/36 (100.00%) · Uncovered code |
CI finish ledger — dec95c9CI fully finished on this head (Finish Workflow + Mergeable Check both pass). Zero test-level failures on the current head; every stateless/integration/build/stress check is green.
Note: the earlier reds on the pre-merge commit Session id: cron:our-pr-ci-monitor:20260713-220000 |
5e701d5
Related: #82473
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed a
LOGICAL_ERROR("New empty part is about to materialize but the directory already exist") that could abort the server in debug and sanitizer builds when aDROP/DETACH/MOVE/REPLACE PARTITIONon a MergeTree table ran after a previous such operation was interrupted (for example a rolled-back transaction or a crash) and left a staletmp_empty_<part>directory behind. The stale directory is now reclaimed instead of failing.Description
The covering empty part created by
MergeTreeData::createEmptyPart()(forDROP/DETACH/MOVE/REPLACE PARTITIONon a non-replicated MergeTree) is written under atmp_empty_<part>directory and renamed to its persistent name only later, via the operation's transaction (rename_in_transaction = true).If the operation is interrupted after the empty part reaches the
PreActivestate (sois_tempis alreadyfalse) but before the deferred rename runs, and the transaction then rolls back, the physicaltmp_empty_<part>directory is left on disk. The rolled-back part is no longer treated as temporary, so its directory is reclaimed only later by the temporary-directory sweeper (temporary_directories_lifetime, one day by default). In CI this was triggered onplain_rewritableobject storage by a fault-injected metadata commit (Cannot schedule a task: fault injected ... While committing metadata operation) during a stress run.A subsequent covering operation on the same still-visible source part derives the same name and
createEmptyPart()found the leftover directory. The old code aborted the server with aLOGICAL_ERROR; the assumption stated in its comment ("New part have to capture its name, therefore there is no concurrency in directory creation") does not hold for such stale leftovers.The in-memory temporary-parts holder acquired just above (
getTemporaryPartDirectoryHolder, which throws if any live operation already holds the name) already guarantees no concurrent operation owns this name, so an existing directory can only be a stale leftover and is safe to remove. The fix reclaims it (logs a warning andremoveRecursive) beforebeginTransaction(), mirroring the regular INSERT path inMergeTreeDataWriter::writeTempPartImpl, which already reclaims a stale temporary directory instead of failing.A regression test (
04000_empty_part_stale_tmp_dir) reproduces the original crash deterministically using a newcreate_empty_part_inject_stale_dirfailpoint that injects the stale directory.Found by the AST fuzzer under the
Stress test (arm_ubsan)job (STID 3202-2fe9). First seen on master at commitdd89f87010a4d59d6278b02d3030683e29940c9f. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?REF=master&sha=dd89f87010a4d59d6278b02d3030683e29940c9f&name_0=MasterCI&name_1=Stress%20test%20%28arm_ubsan%29Version info
26.7.1.893(included in26.7and later)