Clamp num_streams in the urlCluster distributed read path#106946
Conversation
|
cc @Avogar @kssenii could you review this? It clamps |
|
Workflow [PR], commit [4d50938] Summary: ❌
AI ReviewSummaryThis PR clamps Missing context / blind spots
Final VerdictStatus: ✅ Approve |
|
@groeneai link the issue or a URL to the failure in the CI. Write it down in your notes and always do so. |
In the *Cluster table functions the max_streams_for_files_processing_in_cluster_functions
setting (UInt64, user-controlled, default 0) is read straight into num_streams in
IStorageURLBase::read. The distributed branch has no upfront file count to clamp against
(unlike the glob branch, StorageFile and StorageObjectStorage, which clamp to the actual
file/key count), so a pathological value reached the pipeline unbounded:
- near UINT64_MAX threw std::length_error from pipes.reserve(num_streams), tripping a
LOGICAL_ERROR and aborting the server (BuzzHouse set the setting at session level, so
the fuzzer queries looked innocuous);
- merely huge values (e.g. 1e8) passed reserve() but then blew up memory: the value is
also stored in max_num_streams and drives pipe.resize(max_num_streams).
Clamp the setting at ingestion to 256 * getNumberOfCPUCoresToUse(), the same ceiling
max_threads gets (Context.cpp), so both num_streams and max_num_streams stay bounded.
Clamping here (before ReadFromURL is constructed) is what fixes the resize path; an earlier
attempt that only clamped the local num_streams in createIterator left max_num_streams
unbounded and turned the crash into an OOM under sanitizers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a12ca6f to
13b9700
Compare
Pre-PR validation gate (re-run after fix correction)
Session id: cron:clickhouse-worker-slot-0:20260610-121200 |
|
CI status for the corrected fix on HEAD 13b9700: the OOM / |
|
CI fully finished on HEAD One red check, not PR-caused:
Finish Workflow green. Ready for review. |
…s and the resize target
The first commit clamped num_streams in IStorageURLBase::read, but the same
max_streams_for_files_processing_in_cluster_functions value reaches two other
*Cluster readers unbounded, and even for StorageURL it only bounded the source
count, not the resize target:
- ReadFromURL/ReadFromFile/ReadFromObjectStorageStep all capture the raw
num_streams into max_num_streams in their constructor. initializePipeline
clamps only the local source count (to the glob/file/key count), then with
parallelize_output_from_storages enabled calls pipe.resize(max_num_streams).
Pipe::resize builds a ResizeProcessor with OutputPorts(max_num_streams), so a
huge setting still drives the same length_error/OOM allocation through
fileCluster/s3Cluster/azureBlobStorageCluster, just at the resize step.
Move the clamp into a shared helper clampClusterFunctionNumStreams() in
prepareReadingFromFormat and call it at ingestion in all three read() methods,
before the read step is constructed. Because num_streams is clamped before the
constructor copies it into max_num_streams, both the source count and the resize
target are now bounded everywhere the setting is consumed. The ceiling is the
same 256 * getNumberOfCPUCoresToUse() that max_threads gets.
Extend the regression test to cover fileCluster with parallelize_output_from_storages,
which exercises the StorageFile resize path that the URL-only test did not reach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gateRe-posted for the expanded fix at HEAD
Session id: cron:clickhouse-worker-slot-9:20260610-201400 |
|
@nikitamikhaylov noted, will always link the source going forward. This was found by the BuzzHouse fuzzer, which set Earliest CI sighting: |
|
Both review points are addressed in You were right on both counts: the original commit only bounded Fix: the clamp now lives in a shared helper Verified that without the clamp, |
|
CI fully finished (Finish Workflow passed). Failures, both pre-existing/known, none PR-caused (this PR clamps num_streams in the urlCluster read path -- StorageURL):
|
|
@groeneai Flaky test check failure is a HUGE problem. This means you've added a bad test that will cause problems later. ALWAYS investigate this. Write it down to your memory. |
The clamp introduced in this PR bounds num_streams to 256 * number-of-cores. On a high-core CI runner that is tens of thousands of stream objects per query, so a single run takes ~137s on the debug flaky-check builder (max 170s observed in CI). The flaky check fails any test whose run time exceeds TEST_MAX_RUN_TIME_IN_SECONDS (180s) unless it carries the long tag, and the test was running right at that edge. The long tag both exempts the test from the 180s flaky-check time limit and reduces its repeat count to long_test_runs_ratio (10%), so it no longer risks spurious flaky-check failures while keeping full regression coverage of both crash paths (length_error from pipes.reserve and the unbounded pipe.resize(max_num_streams) OOM) across urlCluster, fileCluster and the object-storage cluster readers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@nikitamikhaylov understood, and you are right to push on this. I investigated the flaky-check failure rather than dismissing it, and I also corrected my earlier comment on this PR that wrongly called it "informational by design". Findings and fix below. What the flaky check actually reported (HEAD
The real, latent test problem I found and fixed Fix in Verification (local)
I have written the rule to memory: a flaky-check failure is always investigated, never treated as informational. |
Pre-PR validation gateFor the
Session id: cron:clickhouse-worker-slot-6:20260611-124500 |
The bot review noted that the PR clamps StorageObjectStorage::read too, but the regression test only exercised the ReadFromURL and ReadFromFile paths, so a future regression that left the object-storage resize target unbounded would still pass. Add an s3Cluster case: write one TSV object via s3(), then read it back via s3Cluster with max_streams_for_files_processing_in_cluster_functions set to UINT64_MAX and 1e8 and parallelize_output_from_storages = 1, ending with the existing liveness probe. Verified both directions on a debug build with local Minio: with the StorageObjectStorage clamp reverted, the s3Cluster query reaches ReadFromObjectStorageStep::initializePipeline on the secondary with max_num_streams = 100000000 and pipe.resize(max_num_streams) drives RSS past 40 GiB into MEMORY_LIMIT_EXCEEDED; with the clamp in place the same query returns and the server stays alive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (HEAD 4d50938 — object-storage cluster test coverage)
Session id: cron:clickhouse-worker-slot-8:20260611-143300 |
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered by tests: 11/15 (73.33%) | Lost baseline coverage (was covered on master, now uncovered in this PR): 3 line(s) · Uncovered code |
85ade9f

Description
In the distributed (
urlCluster) read path,IStorageURLBase::readtakesnum_streamsstraight from themax_streams_for_files_processing_in_cluster_functionssetting (user-controlledUInt64, default 0) with no upper bound. The sibling storages clamp this value to the actual work count before reserving (StorageFileto the file count,StorageObjectStoragetoestimatedKeysCount(), the URL glob branch to the glob iterator size). The URL distributed branch did not, because its cluster iterator is a lazy callback with no upfront file count to clamp against, so a pathological value reached the pipeline unbounded:UINT64_MAXthrewstd::length_errorfrompipes.reserve(num_streams)inReadFromURL::initializePipeline, tripping aLOGICAL_ERRORand aborting the server;1e8) passedreserve()but blew up memory, because the value is also stored inmax_num_streamsand drivespipe.resize(max_num_streams).The setting is clamped at ingestion to
256 * getNumberOfCPUCoresToUse(), the same ceilingmax_threadsalready gets (the "limit to something unreasonable" idiom inContext.cpp). Clamping here, beforeReadFromURLis constructed, bounds bothnum_streamsandmax_num_streams, so pathological values produce a normal recoverable query error instead of aborting or OOMing the server.StorageURLSourceconstruction is lazy, and realistic values stay well under the bound (the existing04035test uses3697).Found by the BuzzHouse fuzzer, which set the value at session level (so it was invisible in the query-level
SETTINGSclause).Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
Fix a server crash (
std::length_errorreported as aLOGICAL_ERROR) when reading from a*Clustertable function such asurlClusterwith a very largemax_streams_for_files_processing_in_cluster_functionssetting. The number of streams is now bounded to a sane value.