Account all resources of normalized_query_hash quotas per query hash by alexey-milovidov · Pull Request #107681 · ClickHouse/ClickHouse · GitHub
Skip to content

Account all resources of normalized_query_hash quotas per query hash#107681

Merged
alexey-milovidov merged 15 commits into
masterfrom
fix/normalized-hash-quota-all-resources
Jul 4, 2026
Merged

Account all resources of normalized_query_hash quotas per query hash#107681
alexey-milovidov merged 15 commits into
masterfrom
fix/normalized-hash-quota-all-resources

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Jun 16, 2026

Copy link
Copy Markdown
Member

Related: #107664

A quota KEYED BY normalized_query_hash is meant to give each distinct query pattern its own bucket. Until now only the query-count counters (QUERIES, QUERY_SELECTS, QUERY_INSERTS) were routed through the per-hash intervals via EnabledQuota::usedForQuery. The remaining counters were fed by the pipeline callbacks, which called EnabledQuota::used directly and therefore accounted against the placeholder per-user intervals:

  • READ_ROWS / READ_BYTES from ReadProgressCallback,
  • RESULT_ROWS / RESULT_BYTES / EXECUTION_TIME from LimitsCheckingTransform,
  • WRITTEN_BYTES from CountingTransform (and its pre-check in InterpreterInsertQuery),
  • ERRORS incremented in the exception callbacks of executeQuery.

As a result, for NORMALIZED_QUERY_HASH quotas these resources behaved as a single global per-user bucket instead of being split per query pattern.

This change routes all of them through EnabledQuota::usedForQuery (and a new checkExceededForQuery), which resolves the per-hash intervals for NORMALIZED_QUERY_HASH quotas and the shared session intervals for every other quota — so behavior is unchanged for non-hash quotas. The normalized query hash is carried to the pipeline accounting via QueryPipeline and, for the insert path, via Context.

This addresses the review comment on #107664 that these resources were not bucketed per hash. It is stacked on top of #107664 (it depends on its usedForQuery machinery) and targets that branch; GitHub will retarget it to master once #107664 is merged.

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

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

Quotas keyed by normalized_query_hash now account all resources (read_rows, read_bytes, result_rows, result_bytes, execution_time, written_bytes, errors) per query pattern, like the query-count counters, instead of accounting them against a single shared per-user bucket.

🤖 Generated with Claude Code

Version info

  • Merged into: 26.7.1.508

A quota `KEYED BY normalized_query_hash` is meant to give each distinct
query pattern its own bucket. Until now only the query-count counters
(`QUERIES`, `QUERY_SELECTS`, `QUERY_INSERTS`) were routed through the
per-hash intervals via `usedForQuery`. The remaining counters were fed by
the pipeline callbacks, which called `EnabledQuota::used` directly and so
accounted against the placeholder per-user intervals:

  * `READ_ROWS` / `READ_BYTES` from `ReadProgressCallback`,
  * `RESULT_ROWS` / `RESULT_BYTES` / `EXECUTION_TIME` from `LimitsCheckingTransform`,
  * `WRITTEN_BYTES` from `CountingTransform` (and its pre-check in `InterpreterInsertQuery`),
  * `ERRORS` incremented in the exception callbacks of `executeQuery`.

As a result those resources behaved as a single global per-user bucket for
`NORMALIZED_QUERY_HASH` quotas instead of being split per query pattern.

This change routes all of them through `EnabledQuota::usedForQuery` (and a
new `checkExceededForQuery`), which resolves the per-hash intervals for
`NORMALIZED_QUERY_HASH` quotas and the shared session intervals for every
other quota - so behavior is unchanged for non-hash quotas. The normalized
query hash is carried to the pipeline accounting via `QueryPipeline` and,
for the insert path, via `Context`.

Follow-up to #107667 (re-implemented in #107664), addressing the review
comment that these resources were not bucketed per hash.

A stateless test (`04341_normalized_query_hash_quota_resources`) checks
that two structurally different query patterns get independent `read_rows`
and `result_rows` buckets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexey-milovidov alexey-milovidov changed the base branch from fix/multiple-quotas-per-user to master June 17, 2026 19:05
@clickhouse-gh

clickhouse-gh Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jun 17, 2026
Comment thread tests/queries/0_stateless/04341_normalized_query_hash_quota_resources.sh Outdated
Comment thread src/Access/EnabledQuota.h Outdated
alexey-milovidov and others added 2 commits June 19, 2026 13:37
A user can be governed by several quotas at once (this PR stack enforces
all quotas assigned to a user, not only one). `SHOW CREATE QUOTA` without
an explicit name routed through the single-valued current-quota path
(`Context::getQuotaUsage`) and therefore exposed only whichever quota was
returned first from the unordered set of governing quotas, which was
inconsistent with enforcement. Route it through `getQuotaUsages` so it
lists every quota currently governing the user.

Addresses the review comment on `EnabledQuota::getUsage`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`04341_normalized_query_hash_quota_resources` only exercised `read_rows`
and `result_rows`, but the change also routes the independently-wired
`written_bytes` (the insert `CountingTransform` and its pre-check) and
`errors` (the exception callbacks) counters through the
per-normalized-query-hash intervals. Add focused cases showing two query
patterns get independent buckets for both, so every resource named in the
changelog has direct evidence.

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

Copy link
Copy Markdown
Member Author

Addressed the AI review's two required actions (commits bf4c908, b64f595):

  • SHOW CREATE QUOTA introspection (EnabledQuota.h): the current-quota form now routes through getQuotaUsages and lists every quota governing the user, consistent with the multi-quota enforcement, instead of an arbitrary single one. New test 05019_show_create_quota_lists_all_current_quotas (user assigned two quotas of different key types → both shown). The single-valued getUsage/getQuotaUsage are kept to avoid touching possible downstream consumers.
  • Test coverage: 04341_normalized_query_hash_quota_resources now also covers written_bytes (insert CountingTransform + pre-check) and errors (exception callbacks), each demonstrating independent per-hash buckets for two query patterns. execution_time is omitted as non-deterministic.

Both findings validated locally against a freshly built server.

The two CI reds are unrelated flakes: the Stress test (amd_tsan) Unexpected exception in refresh scheduling is the known open #106737 (MaterializedView refresh scheduling, the other two stress sub-failures cascade from it), and the Performance Comparison array_reduce slowdowns are perf noise (the cross-shard verdict is "No significant performance changes detected").

Comment thread src/Interpreters/InterpreterInsertQuery.cpp
alexey-milovidov and others added 3 commits June 21, 2026 12:39
The async insert flush in `AsynchronousInsertQueue::processData` builds
`InterpreterInsertQuery` directly instead of going through `executeQuery`, so
it never set `normalized_query_hash` on the flush context. As a result the
`WRITTEN_BYTES` pre-check in `InterpreterInsertQuery` and the `CountingTransform`
both saw hash `0`, and a `KEYED BY normalized_query_hash` quota accounted every
async insert pattern against a single shared bucket instead of one per pattern.

Set the hash on the flush context right after it is computed, mirroring what
`executeQuery` does for the synchronous path.

New test `04403_normalized_query_hash_quota_async_insert` inserts the same data
with two structurally different INSERT statements (the `Values` and the
`JSONEachRow` formats, which produce different normalized hashes) under a
`MAX written_bytes` quota and checks their buckets are independent; it fails if
the flush context keeps using hash `0` (verified by reverting the one-line fix).

Addresses #107681 (comment)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After merging master, `05019_show_create_quota_lists_all_current_quotas` was far
above the highest existing test number (the `05019` slot came from unrelated
local working-tree files), so `test_numbers_check` failed with
`Gap (4364, 5019) > 100`. Renumber it to `04402`, the next free slot.

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

Copy link
Copy Markdown
Member Author

Addressed the AI review's required action (async-insert written_bytes bucketing) and merged master.

  • Async insert flush hash (294ad3494e5): AsynchronousInsertQueue::processData now sets normalized_query_hash on the flush context before constructing InterpreterInsertQuery, mirroring executeQuery. Previously async inserts charged WRITTEN_BYTES to hash 0, so all async patterns shared one bucket under a KEYED BY normalized_query_hash quota. New test 04403_normalized_query_hash_quota_async_insert (Values vs JSONEachRow → distinct hashes) verifies the buckets are independent; I confirmed it fails when the one-line fix is reverted and passes with it. (Inline thread r3442853435 resolved.)
  • Merged master (was behind, with a red style check). The merge was conflict-free.
  • test_numbers_check gap (f77a41d93ca): the SHOW CREATE QUOTA test landed at 05019 (an artifact of unrelated local working-tree files), far above the highest real test number, so the style check failed with Gap (4364, 5019) > 100. Renumbered it to 04402.

Built locally and re-ran 04341, 04402 and the new 04403 against a freshly built server — all pass.

Comment thread src/Interpreters/InsertDependenciesBuilder.cpp
Comment thread src/Interpreters/InterpreterInsertQuery.cpp
Comment thread src/Interpreters/IInterpreterUnionOrSelectQuery.cpp
…-quota-all-resources

# Conflicts:
#	src/Interpreters/Access/InterpreterShowCreateAccessEntityQuery.cpp
The PR added `Context::normalized_query_hash` as a member with an in-class
initializer (`= 0`), but the `ContextData` copy constructor did not list it,
so `Context::createCopy` silently reset it to `0` instead of copying it.

The INSERT path replaces its context with `Context::createCopy(context)` to
disable parallel replicas for the write (`InterpreterInsertQuery.cpp`, the
`canUseParallelReplicasOnInitiator` branch). With parallel replicas enabled,
that copy therefore lost the hash, so the `WRITTEN_BYTES` accounting in
`CountingTransform` charged every distinct INSERT SELECT pattern to the shared
hash-`0` bucket. A second, structurally different pattern then inherited the
first pattern's already-exhausted bucket and was wrongly rejected with
`QUOTA_EXCEEDED`.

This is exactly what `04341_normalized_query_hash_quota_resources` and
`04403_normalized_query_hash_quota_async_insert` exercised, and they failed
deterministically only in the parallel-replicas CI configuration. Read-side
counters (`READ_ROWS`/`RESULT_ROWS`) were unaffected because they flow through
`pipeline.setNormalizedQueryHash` on the un-copied initiator pipeline.

Copy the member in the copy constructor so a copied context keeps the query
hash, which is the intended copy semantics.

CI report: #107681
…efreshable materialized-view contexts

Per-hash quota accounting was lost in derived execution contexts that are built
outside the normal `executeQuery` hash propagation:

- `SQL SECURITY DEFINER`/`NONE` views and materialized views run their bodies
  under a context created by `StorageInMemoryMetadata::getSQLSecurityOverriddenContext`
  from the global context, where `normalized_query_hash` is `0`. The dependent
  materialized-view insert (`InsertDependenciesBuilder`) and the definer view read
  (`IInterpreterUnionOrSelectQuery`) therefore charged the definer's
  `KEYED BY normalized_query_hash` quota to a single shared hash-`0` bucket.
  Carry the outer query's hash into the overridden context, next to the existing
  `ProgressCallback`/`ProcessListElement` propagation.

- Refreshable materialized views compute the refresh query hash for logging but
  never set it on `refresh_context`, so `RefreshTask` charged the refresh write
  (the `WRITTEN_BYTES` pre-check and `CountingTransform`) to hash `0`. Set the
  computed hash on `refresh_context` before constructing `InterpreterInsertQuery`,
  mirroring the direct-query and async-insert paths.

Together with preserving `normalized_query_hash` in the `Context` copy constructor,
this routes the resource counters of these contexts through the per-hash intervals.

Adds `04404_normalized_query_hash_quota_sql_security`, which verifies that
`written_bytes` of a `SQL SECURITY DEFINER` materialized view is bucketed per the
outer insert pattern. Verified that the per-hash bucket key equals the outer
insert's `normalizedQueryHash` (i.e. the hash is propagated, not `0`).

CI report: #107681
@alexey-milovidov

Copy link
Copy Markdown
Member Author

Picked this up after #107664 merged into master (the branch was then CONFLICTING). Pushed f77a41d93ca..35b70700fde:

  1. Re-merged master — the only conflict was in InterpreterShowCreateAccessEntityQuery.cpp, where master had independently adopted the same multi-quota getQuotaUsages() loop (via Enforce all quotas assigned to a user instead of only one #107664); kept the loop + the explanatory comment.

  2. Fixed the two red tests 04341_normalized_query_hash_quota_resources and 04403_normalized_query_hash_quota_async_insert (deterministic, only in the ParallelReplicas config). Root cause: the ContextData copy constructor did not copy the new normalized_query_hash member, so Context::createCopy reset it to 0. Under parallel replicas the insert path does Context::createCopy(context) to disable parallel replicas for the write (InterpreterInsertQuery), so the WRITTEN_BYTES accounting charged every INSERT SELECT pattern to the shared hash-0 bucket. One-line fix in the copy constructor (10669e4b30d).

  3. Addressed the AI review ⚠️ Request changes Majors (35b70700fde) — the SQL-security/refreshable-MV contexts that still dropped the hash:

    • getSQLSecurityOverriddenContext now carries the outer query hash into the DEFINER/NONE overridden context (covers the definer view-read and the materialized-view dependency-insert sites).
    • RefreshTask now sets the computed refresh hash on refresh_context before building the insert.
    • New test 04404_normalized_query_hash_quota_sql_security verifies a SQL SECURITY DEFINER materialized view's written_bytes is bucketed per outer insert pattern; I confirmed the bucket key equals the outer insert's normalizedQueryHash (propagated, not 0), and verified the refreshable-MV path produces distinct per-hash buckets.

All review threads resolved. The two remaining stress-test reds are known master-wide flakes — Unexpected exception in refresh scheduling (#106737) and Hung check failed (#107941) — unrelated to this change; a fresh CI run on the re-merged branch will re-test them.

Comment thread src/Access/EnabledQuota.h Outdated
…td::vector`

The `EnabledQuota::usedForQuery` overload set had separate two-pair and
three-pair `std::pair<QuotaType, QuotaValue>` variants, which was ugly and
did not scale. Replace them with a single
`usedForQuery(UInt64, const std::vector<std::pair<QuotaType, QuotaValue>> &, bool)`
overload, mirroring the existing `EnabledQuota::used` vector overload. The
single-counter `usedForQuery(UInt64, QuotaType, QuotaValue, bool)` convenience
overload is kept. Call sites in `ReadProgressCallback` and
`LimitsCheckingTransform` pass a braced list of usages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/QueryPipeline/QueryPipeline.h
Comment thread src/Access/EnabledQuota.h Outdated

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The changes are mostly trivial. LGTM, but see the comments about performance.

@alexey-milovidov alexey-milovidov self-assigned this Jun 30, 2026
alexey-milovidov and others added 2 commits June 30, 2026 06:56
Address review (thread on `QueryPipeline.h:134`): the empirical-scan
pipeline in `WhatIfIndexEstimator` called `setQuota` but never set the
normalized query hash, so under a `KEYED BY normalized_query_hash` quota
the scan's `read_rows`/`read_bytes` were accounted against the shared
hash-`0` bucket. Two structurally different `EXPLAIN WHATIF` patterns
therefore shared one bucket, and once one exhausted it the other was
rejected too.

`WhatIfIndexEstimator` now calls `setNormalizedQueryHash` next to
`setQuota`, so the scan is accounted against the query's own per-hash
bucket. The `Context` copy made in `WhatIfIndexEstimator::run` already
preserves the hash.

New test `04490_normalized_query_hash_quota_whatif` checks that two
structurally different `EXPLAIN WHATIF` patterns get independent
`read_rows` buckets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review (performance comment on `EnabledQuota.h:66`): the
multi-counter `EnabledQuota::usedForQuery` overload took a
`std::vector`, while its two hot-path callers — `ReadProgressCallback`
and `LimitsCheckingTransform` — pass braced lists on every read-progress
and result chunk. That constructed a heap-allocating vector on each call
before the `empty` fast path could be reached, and ordinary authenticated
contexts usually carry a non-null `EnabledQuota`.

Switch the parameter to `std::initializer_list<std::pair<QuotaType,
QuotaValue>>`, whose backing array has automatic storage, so the braced
lists at the call sites no longer allocate. The single clean signature is
preserved (no fixed-arity overload zoo), and behavior is unchanged.

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

Copy link
Copy Markdown
Member Author

Addressed the AI review's ⚠️ Request changes (both required actions) and merged master. Pushed 861237c3503..e8736c8897a:

  1. EXPLAIN WHATIF per-hash accounting (8735d389174): the empirical-scan pipeline in WhatIfIndexEstimator called setQuota but never set the normalized query hash, so under a KEYED BY normalized_query_hash quota the scan's read_rows/read_bytes went to the shared hash-0 bucket, and two structurally different EXPLAIN WHATIF patterns shared one bucket. It now calls setNormalizedQueryHash(context->getNormalizedQueryHash()) next to setQuota (the Context copy in WhatIfIndexEstimator::run already preserves the hash). New test 04490_normalized_query_hash_quota_whatif proves two structurally different EXPLAIN WHATIF patterns get independent read_rows buckets.

  2. Allocation-free multi-counter usedForQuery (e8736c8897a): the std::vector overload from 861237c3503 was switched to std::initializer_list<std::pair<QuotaType, QuotaValue>>. The braced lists passed by the hot-path callers (ReadProgressCallback::onProgress, LimitsCheckingTransform::checkQuota) now use stack-backed storage and no longer heap-allocate before the empty fast path — while keeping the single combined signature (no overload zoo). All three review threads are addressed and resolved.

  3. Merged master (the branch was ~1 week behind and red). The merge was conflict-free; the net diff vs master is exactly the PR's own files plus the two fixes above.

The previous red was the Stress test (arm_msan) Logical error: 'Unexpected exception in refresh scheduling' (STID 2508-3e7b) and the two stress sub-failures cascading from it (the server couldn't start). That is the known #106737 in RefreshTask::doScheduling, unrelated to quotas; it was CLOSED on 2026-06-29, fixed by #108441, whose fix (931d5fd893d) is now included via the master merge — so the fresh CI run picks it up.

@alexey-milovidov

Copy link
Copy Markdown
Member Author

CI on the current head e8736c8897a is still finishing; the reds so far are all unrelated to this quota change, and each is already tracked:

AI review is ✅ Approve on this commit and there are no unresolved review threads, so the PR is ready modulo CI completion and merge.

@alexey-milovidov

Copy link
Copy Markdown
Member Author

Re-triggered the failed CI jobs on head `e8736c8897a` (run 28426257065) — the remaining reds are confirmed unrelated flakes:

No re-merge needed (merge-base is from today, <1 day old). AI review is ✅ Approve, all review threads are resolved, and the PR is MERGEABLE. Ready modulo the flaky reruns going green and merge.

@alexey-milovidov

Copy link
Copy Markdown
Member Author

The latest CI on head e8736c8897a (run 28426257065, completed 2026-07-01 01:56 UTC) re-hit only the known master-wide flake:

  • Stress test (amd_tsan / arm_tsan) → Hung check failed, possible deadlock found = #107941 (OPEN). Both are bot-linked to it; the hung stacks are in executeASTFuzzerQueriesQueryAnalyzer (query analysis, before pipeline execution), and none of the stress logs contain any quota frames (EnabledQuota / usedForQuery / checkExceededForQuery / ReadProgressCallback / LimitsCheckingTransform / CountingTransform), so the new accounting paths are not involved. These are non-required checks, so the PR is MERGEABLE past them.

I re-triggered the failed stress jobs to give them a fresh roll. No re-merge: the merge-base is still <1 day old, and the only PR-touched file master has evolved since then is Context.cpp, whose new commits are in the background-schedule-pool area — disjoint from this PR's ContextData copy-constructor / normalized_query_hash changes, and the branch remains conflict-free.

AI review is ✅ Approve on this commit and there are no unresolved review threads, so the PR is ready modulo the flaky reruns and merge.

Keep the branch current (3 days behind) and re-trigger CI. The only
CI reds were the known master-wide flake 'Hung check failed, possible
deadlock found' (#107941) on amd_tsan/arm_tsan; verified the hung
thread is the AST-fuzzer 'formatQuery' query (0 rows read) with no
quota frames, unrelated to this change. Merge is conflict-free; net
diff vs master is exactly this PR's files.

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

Copy link
Copy Markdown
Member Author

Re-merged master (branch was 3 days behind, merge-base 2f4799932b5 from 2026-06-30) and pushed e8736c8897a..05d90ad53de to give the flaky stress checks a fresh CI roll.

The merge was conflict-free — the six PR-touched files that master has since evolved (Context.cpp/.h, executeQuery.cpp, QueryPipeline.cpp, RefreshTask.cpp, WhatIfIndexEstimator.cpp) auto-merged cleanly, and the net diff vs master is unchanged (34 files, +503/-31, exactly this PR's own files).

The previous head e8736c8897a was red only on the known master-wide flake #107941 Hung check failed, possible deadlock found (Stress test (amd_tsan) and (arm_tsan)). I re-confirmed it is unrelated to this change: both architectures hung on the identical AST-fuzzer query SELECT formatQuery($$SELECT [[[[…$$) (deeply nested arrays, read_rows = 0, is_cancelled = 1), and neither hung_check.log contains any quota frames (EnabledQuota / usedForQuery / checkExceededForQuery / ReadProgressCallback / LimitsCheckingTransform / CountingTransform). These are non-required checks, so the PR stays MERGEABLE.

AI review is ✅ Approve on the prior head and there are no unresolved review threads, so the PR is ready modulo the fresh CI going green and merge.

Comment thread src/QueryPipeline/ReadProgressCallback.cpp
The `dictGetKeys` const-path builds its own `ReadProgressCallback` and set the
quota but not the normalized query hash, so its dictionary scan charged
`READ_ROWS`/`READ_BYTES` to the shared hash-`0` bucket. Under a
`KEYED BY normalized_query_hash` quota two structurally different `dictGetKeys`
patterns therefore shared one read bucket, contradicting this PR's per-hash
contract for the read-progress counters.

Propagate the query's normalized hash into that callback, mirroring the earlier
fix for the `EXPLAIN WHATIF` empirical-scan path. Add
`04498_dict_get_keys_normalized_query_hash_quota` proving two structurally
different `dictGetKeys` patterns get independent `read_rows` buckets (`read_rows`
and `read_bytes` are routed through the same callback).

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

Copy link
Copy Markdown
Member Author

Addressed the AI review's ⚠️ Request changes verdict on the fresh head — pushed 6d928ab9238.

The re-review surfaced one more direct ReadProgressCallback caller that was missed until now: FunctionDictGetKeys::executeConstPath set the quota but not the normalized query hash, so a dictGetKeys dictionary scan charged READ_ROWS/READ_BYTES to the shared hash-0 bucket. That is the same class of gap already fixed for the EXPLAIN WHATIF empirical-scan path.

  • Fix: propagate helper.context->getNormalizedQueryHash() into that callback (one line, next to setQuota).
  • Test: 04498_dict_get_keys_normalized_query_hash_quota — under a KEYED BY normalized_query_hash MAX read_rows quota, two structurally different dictGetKeys patterns get independent read_rows buckets (verified locally: the second call of pattern A hits QUOTA_EXCEEDED, while a differently-shaped pattern B stays within its own bucket).

Also noticed on this fresh CI roll: the 30+-pass Hung check flake (#107941) is now green on all 11 stress configs, confirming it was the stale-binary flake. The only remaining reds are unrelated master-wide flakes — 04319_skip_unavailable_shards_mode_table_missing on arm_darwin (already tracked by #109254) and the Upgrade check backup-encryption error (~120 PRs over 7d).


auto transform = std::make_shared<LimitsCheckingTransform>(output->getSharedHeader(), limits);
transform->setQuota(quota_);
transform->setNormalizedQueryHash(normalized_query_hash);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

setLimitsAndQuota still leaves one read-progress path outside quota accounting. This method only wires the LimitsCheckingTransform; it does not populate QueryPipeline::quota, and PullingPipelineExecutor later builds the ReadProgressCallback from pipeline.getReadProgressCallback(), which reads that member. InterpreterWatchQuery::execute is a current caller that only uses setLimitsAndQuota, so its callback still sees quota == nullptr and never calls usedForQuery for READ_ROWS / READ_BYTES.

That means a WATCH query can still bypass MAX read_rows / MAX read_bytes quotas entirely, including the KEYED BY normalized_query_hash case this PR is trying to make fully per-pattern. Please either store quota_ in the pipeline here, or make the watch path call setQuota before execution, and add a focused WATCH regression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed the mechanism: setLimitsAndQuota wires the quota only into the LimitsCheckingTransform (the result-side counters RESULT_ROWS / RESULT_BYTES / EXECUTION_TIME), while getReadProgressCallback reads QueryPipeline::quota, which setLimitsAndQuota does not populate. So a pipeline configured only via setLimitsAndQuota — the WATCH path — gets a read-progress callback with quota == nullptr.

This is pre-existing behavior, not a regression from this change. On master, setLimitsAndQuota is identical (it sets only transform->setQuota), and InterpreterWatchQuery::execute has always called only setLimitsAndQuota, never setQuota. So READ_ROWS / READ_BYTES were never accounted for WATCH via the read-progress path, for any quota kind. This PR only adds setNormalizedQueryHash on that path, which makes the already-accounted result-side counters bucket per hash; it does not change read-side accounting for WATCH.

Concretely, this is not the "resource went to the shared per-user bucket instead of the per-hash bucket" bug this PR fixes — for that, the resource would have to be accounted against some interval, and here READ_ROWS / READ_BYTES for WATCH are not accounted at all.

Storing quota_ in the pipeline in setLimitsAndQuota would newly enable READ_ROWS / READ_BYTES quota enforcement for WATCH, which is a behavior change beyond the scope of this fix. Leaving it out of this PR; it can be a focused follow-up (store quota_ in the pipeline in setLimitsAndQuota, plus a WATCH read-quota regression test).

@clickhouse-gh

clickhouse-gh Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

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

Changed lines: Changed C/C++ lines covered: 151/157 (96.18%) · Uncovered code

Full report · Diff report

@alexey-milovidov

Copy link
Copy Markdown
Member Author

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good.

@alexey-milovidov alexey-milovidov added this pull request to the merge queue Jul 4, 2026
Merged via the queue into master with commit 211cd0b Jul 4, 2026
343 of 344 checks passed
@alexey-milovidov alexey-milovidov deleted the fix/normalized-hash-quota-all-resources branch July 4, 2026 02:39
@robot-ch-test-poll3 robot-ch-test-poll3 added the pr-synced-to-cloud The PR is synced to the cloud repo label Jul 4, 2026
alexey-milovidov added a commit that referenced this pull request Jul 4, 2026
Resolve the conflict in `InterpreterInsertQuery.cpp` caused by master's
`NORMALIZED_QUERY_HASH` quota change (#107681), which added
`context->getNormalizedQueryHash()` to the `CountingTransform` constructor
call in `buildInsertPipeline`.

This PR restructured `buildInsertPipeline` to fan out the write side across
parallel sink chains: the `CountingTransform` was relocated from the sink
chain (`chain.addSource(counting)`) to the single-stream pipeline head, and
the old `chain` block was replaced by the `ResizeProcessor` distribution
logic. The resolution keeps the parallel fan-out and propagates master's
per-query-hash accounting to the relocated head-stream `CountingTransform`,
so `WRITTEN_BYTES` for the new parallel plain-`INSERT` path is bucketed per
`NORMALIZED_QUERY_HASH` just like the `INSERT SELECT` path. The disputed
fail-close `strict_dedup_single_stream` guard is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-bugfix Pull request with bugfix, not backported by default 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.

2 participants