Batch ZooKeeper multi-requests in clearBlocksInPartition by cmcclure-twilio · Pull Request #105991 · ClickHouse/ClickHouse · GitHub
Skip to content

Batch ZooKeeper multi-requests in clearBlocksInPartition#105991

Merged
alexey-milovidov merged 9 commits into
ClickHouse:masterfrom
cmcclure-twilio:fix-truncate-zk-multi-size
Jul 3, 2026
Merged

Batch ZooKeeper multi-requests in clearBlocksInPartition#105991
alexey-milovidov merged 9 commits into
ClickHouse:masterfrom
cmcclure-twilio:fix-truncate-zk-multi-size

Conversation

@cmcclure-twilio

@cmcclure-twilio cmcclure-twilio commented May 27, 2026

Copy link
Copy Markdown
Contributor

clearBlocksInPartition removed all deduplication block entries for a partition in a single ZooKeeper multi request. For tables with a large replicated_deduplication_window, that request could exceed ZooKeeper's jute.maxbuffer limit (1MB by default), causing TRUNCATE TABLE and DROP PARTITION to fail.

The removals are now split into batches of zkutil::MULTI_BATCH_SIZE (100) operations, with up to 16 batches in flight at once, consistent with other bulk-removal paths in the codebase such as removeChildren and tryRemoveChildrenRecursive. Non-user (hardware) ZooKeeper errors from a batch are propagated instead of being silently ignored, preserving the failure semantics of the original tryMulti-based implementation.

Changelog category (leave one):

  • Improvement

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Only if you used Apache ZooKeeper (not recommended) instead of ClickHouse Keeper. Fix TRUNCATE TABLE and DROP PARTITION failing on tables with many deduplication blocks, where removing them in a single ZooKeeper request could exceed the default 1MB jute.maxbuffer limit.

Documentation entry for user-facing changes

  • Documentation is written (mandatory for new features)

Version info

  • Merged into: 26.7.1.485

  clearBlocksInPartition sends Remove requests for all deduplication
  block entries in a partition as a single ZooKeeper multi-request.
  For tables with large deduplication windows this can cause
  multi-requests exceeding ZooKeeper's jute.maxbuffer limit (default
  1MB), causing TRUNCATE TABLE and DROP PARTITION to fail.

  The fix batches the Remove operations using zkutil::MULTI_BATCH_SIZE
  (100 ops per batch), consistent with other bulk-removal paths in the
  codebase such as removeChildren and `ryRemoveChildrenRecursive.
@Michicosun Michicosun self-assigned this May 28, 2026
@Michicosun Michicosun added the can be tested Allows running workflows for external contributors label May 28, 2026
@clickhouse-gh

clickhouse-gh Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label May 28, 2026
Comment thread tests/queries/0_stateless/4061_truncate_many_dedup_blocks.sql Outdated
Comment thread src/Storages/StorageReplicatedMergeTree.cpp Outdated
…ally

Two pieces of review feedback on the deduplication-block batching change:

1. The previous test inserted only 200 single-row blocks and asserted just
   the row counts, so it passed both before and after the fix (the old single
   `tryMulti` request stayed under the default `jute.maxbuffer`). The test now
   adds a focused assertion that batching actually happened: it groups the
   `Remove` sub-requests for the table's deduplication nodes in
   `system.zookeeper_log` by `(session_id, xid)` (each multi-request shares one
   `xid`) and checks that no multi-request exceeds `MULTI_BATCH_SIZE` and that
   more than one batch was sent. The pre-fix code emits a single 200-op
   multi-request and fails this assertion (`0 0` instead of `1 1`), independent
   of `jute.maxbuffer`.

2. `clearBlocksInPartition` sent the batches strictly sequentially. It now
   keeps up to 16 batches in flight at once using `asyncTryMultiNoThrow`,
   waiting on the oldest batch before issuing a new one, so partitions with
   many deduplication blocks are cleared without a long chain of round-trips.
Comment thread src/Storages/StorageReplicatedMergeTree.cpp Outdated
cmcclure-twilio and others added 2 commits June 10, 2026 16:07
The batched rewrite of `clearBlocksInPartition` switched from `tryMulti` to
`asyncTryMultiNoThrow`, which changed the failure contract. `tryMulti` throws
on non-user (hardware) errors such as `ZCONNECTIONLOSS`, `ZSESSIONEXPIRED` and
`ZOPERATIONTIMEOUT`, but the new code only inspected per-op errors. On session
finalization the sub-responses default to `ZOK`, so a top-level hardware error
was logged as nothing and the caller proceeded to truncate the caches and
enqueue the `DROP_RANGE`/`TRUNCATE` as if all dedup nodes had been removed.
That can leave block IDs in ZooKeeper after a successful `TRUNCATE`/`DROP
PARTITION`, making later reinserts of the same blocks look like duplicates.

Restore the original semantics: re-throw on non-user top-level errors and only
log/ignore expected per-op user errors (e.g. `ZNONODE` for an already-removed
block).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The test created its deduplication blocks with a single
`INSERT ... SELECT FROM numbers(200)`. Despite `max_insert_block_size = 1`,
server-side squashing collapses that into a single part and therefore a single
deduplication block node in ZooKeeper. The number of dedup block nodes equals
the number of separate `INSERT` statements, not the number of rows or parts, so
`TRUNCATE` only had one node to remove: it used a single multi-request and the
batching assertion printed `1 0` instead of `1 1`, failing deterministically.

Convert the test to a shell test that issues 120 separate `INSERT` statements
(more than `zkutil::MULTI_BATCH_SIZE` = 100), which creates 120 `blocks` and
120 `deduplication_hashes` nodes. `TRUNCATE` must then split the removals into
several multi-requests, and the assertion distinguishes the pre-fix behaviour
(one oversized multi-request) from the fixed behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Storages/StorageReplicatedMergeTree.cpp
@cmcclure-twilio

Copy link
Copy Markdown
Contributor Author

@Michicosun thanks for the review — both points are addressed:

  1. Deterministic batching assertion. The test is now 4061_truncate_many_dedup_blocks.sh, issuing 120 separate INSERTs (one dedup block node each, > MULTI_BATCH_SIZE = 100) and asserting via system.zookeeper_log that the Remove sub-requests were split across more than one multi, with no multi exceeding 100 ops. Pre-fix prints 0 0 (fails), fixed prints 1 1, independent of jute.maxbuffer. (commits 71d9ebe, 43dd935)
  2. Parallelism. clearBlocksInPartition now keeps up to 16 batches in flight via asyncTryMultiNoThrow instead of running them strictly sequentially. (commit 71d9ebe)

I also restored the original failure contract after the async switch (re-throw on non-user/hardware Keeper errors) in 52dd789.

Could you take another look when you have a chance?

On the failing CI

  • Stateless tests (arm_asan_ubsan, azure, sequential) — unrelated flaky crash. The segfault is in the object-storage metadata write path (MetadataStorageFromDiskTransactionOperationsWriteBufferFromFile dtor → ASan free) on a background INSERT/merge thread ((no query)), on the azure+ASan build. The new test 4061_truncate_many_dedup_blocks does not appear anywhere in that shard's report or server log, and it isn't scheduled in the sequential shard at all. This looks like infra/flaky and should clear on a re-run.
  • AI Review — other dedup-cleanup callers. Acknowledged in the inline thread; proposing to keep this PR scoped to TRUNCATE/DROP PARTITION and handle dropPartImpl / shard-move paths in a follow-up, since those commit removals atomically with their log entry and need a separate design.

Comment thread src/Storages/StorageReplicatedMergeTree.cpp Outdated
Comment thread src/Storages/StorageReplicatedMergeTree.cpp Outdated
Comment thread src/Storages/StorageReplicatedMergeTree.cpp Outdated
Apply review feedback on `clearBlocksInPartition`:

- In a failed ZooKeeper multi-request, only the first failed operation carries
  a valid error code; the remaining sub-responses are filled with
  `ZRUNTIMEINCONSISTENCY`. Iterating over every sub-response therefore logged
  misleading errors. Use `zkutil::getFailedOpIndex` to find and log only the
  first genuinely failed operation, consistent with other multi-request error
  handling in this file.
- Trim the verbose batching and failure-contract comments down to a single
  line as requested.

The failure contract is unchanged: non-user (hardware) errors still propagate
via `KeeperException`, while expected per-op user errors (e.g. `ZNONODE` for an
already-removed block) are logged and ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cmcclure-twilio

Copy link
Copy Markdown
Contributor Author

@Michicosun thanks for the review — all three points addressed in 7a69e5e:

  1. Log only the first failed op. Now uses zkutil::getFailedOpIndex(response.error, response.responses) and logs just that op's path/error, instead of iterating all sub-responses (the rest carry ZRUNTIMEINCONSISTENCY).
  2. Removed the failure-contract comment.
  3. Trimmed the batching comment to a single line.

The failure contract is unchanged: non-user (hardware) errors still propagate via KeeperException; expected per-op user errors (e.g. ZNONODE) are logged and ignored. Could you take another look when you have a chance?

@Michicosun Michicosun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Comment thread src/Storages/StorageReplicatedMergeTree.cpp Outdated
Comment thread tests/queries/0_stateless/4061_truncate_many_dedup_blocks.sh Outdated
groeneai added a commit to groeneai/ClickHouse that referenced this pull request Jun 29, 2026
The Upgrade check (amd_release) server.log scan fails on benign
background-mutation errors left behind by
02597_column_update_tricky_expression_and_replication: an
`ALTER TABLE test UPDATE d = d + throwIf(1)` with mutations_sync=0
leaves an intentionally-failing mutation that the upgraded server
retries on restart before the test's later KILL MUTATION cleans it up,
logging FUNCTION_THROW_IF_VALUE_IS_NON_ZERO at <Error>.

This is the issue ClickHouse#39174 class (bad mutation, not a backward
incompatibility). The CANNOT_PARSE_TEXT sibling ('x' as UInt64) is
already in the allow-list on master; this adds the throwIf counterpart.
The narrow inner-text 'Value passed to throwIf function is non-zero'
matches all three emitted line shapes (MutatePlainMergeTreeTask, its
executeStep(), and the MergeTreeBackgroundExecutor wrapper) without
masking a genuinely different background-mutation error.

Validated against real leaked upgrade_error_messages.txt (159 lines from
PR ClickHouse#106386, 255 from PR ClickHouse#105991): without the entry every line leaks and
the gate fails; with it 0 lines leak; a control LOGICAL_ERROR in the same
MutatePlainMergeTreeTask still surfaces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread tests/queries/0_stateless/4061_truncate_many_dedup_blocks.sh
@clickhouse-gh

clickhouse-gh Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 85.50% 85.50% +0.00%
Functions 92.70% 92.70% +0.00%
Branches 77.70% 77.70% +0.00%

Changed lines: Changed C/C++ lines covered: 27/33 (81.82%) · Uncovered code

Full report · Diff report

@alexey-milovidov

Copy link
Copy Markdown
Member

The test does not reproduce the bug with ClickHouse Keeper.
It looks only relevant to ZooKeeper. I will change the category to "Improvement".

@alexey-milovidov alexey-milovidov removed the pr-bugfix Pull request with bugfix, not backported by default label Jul 3, 2026
@alexey-milovidov alexey-milovidov merged commit 293593f into ClickHouse:master Jul 3, 2026
171 of 174 checks passed
@robot-clickhouse robot-clickhouse added the pr-synced-to-cloud The PR is synced to the cloud repo label Jul 3, 2026
@clickgapai

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants