Cancel stale ClickHouse queries server-side on navigation#150
Cancel stale ClickHouse queries server-side on navigation#150
Conversation
When users switch time ranges, the refinement pass queries (full table scans without sampling) continued running on the ClickHouse server even after the browser aborted the fetch. These queries consumed server resources and starved new queries, causing perceived slowness. This adds server-side query cancellation via KILL QUERY. Each request context tags its queries with a unique query_id prefix, and when a new context replaces the old one, it sends KILL QUERY to cancel the still-running server queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lars Trieloff <lars@trieloff.net>
Preview deploymentPreview is live at: https://klickhaus.aemstatus.net/preview/pr-150/dashboard.html Updated for commit 383fa85 |
There was a problem hiding this comment.
Pull request overview
This PR adds server-side cancellation for stale ClickHouse queries when a request context is replaced (e.g., when users change dashboard time ranges), preventing long-running refinement queries from continuing to consume resources after the browser aborts the request.
Changes:
- Tag ClickHouse queries with a context-specific
query_idprefix to enable targeted cancellation. - On starting a new request context, send
KILL QUERYfor the previous context’s query group. - Update breakdown tests to ignore control-plane
KILL QUERYPOST calls when asserting query POST behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| js/request-context.js | Adds per-context query group prefixing and triggers server-side cancellation of the previous context’s queries. |
| js/api.js | Stores per-signal query-group metadata and applies query_id tagging; adds killQueryGroup() to issue KILL QUERY. |
| js/breakdowns/index.test.js | Filters mock fetch call assertions to exclude KILL QUERY control requests. |
| js/breakdowns/approx-top.test.js | Same as above for approx-top refinement tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function killQueryGroup(prefix) { | ||
| if (!prefix || !state.credentials?.user) return; | ||
| const safePfx = prefix.replace(/'/g, "\\'"); | ||
| fetch(CLICKHOUSE_URL, { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Basic ${btoa(`${state.credentials.user}:${state.credentials.password}`)}`, | ||
| }, | ||
| body: `KILL QUERY WHERE query_id LIKE '${safePfx}-%' ASYNC`, | ||
| }).catch(() => { /* ignore */ }); |
There was a problem hiding this comment.
killQueryGroup() builds a ClickHouse statement using LIKE '${safePfx}-%', but safePfx only escapes single quotes. If the prefix can contain % or _ (e.g., via dynamic scopes like facet:${breakdown.id}), the LIKE will treat them as wildcards and may cancel queries outside the intended group. Consider either sanitizing the prefix to a strict safe charset before storing it, or escaping %/_ for LIKE (and also escaping backslashes) so the kill pattern only matches the exact prefix.
|
|
||
| // Filter helper: extract only ClickHouse data query POSTs (exclude KILL QUERY calls) | ||
| const queryPosts = (calls) => calls.filter((c) => c.options?.method === 'POST' && c.options?.body?.includes('FORMAT JSON')); | ||
|
|
There was a problem hiding this comment.
The tests introduce two slightly different queryPosts helpers (const arrow in index.test.js vs function in approx-top.test.js) with the same filtering logic. To reduce duplication and keep future changes consistent (e.g., if the ClickHouse POST body format changes), consider extracting a shared helper (or importing from a common test utility) rather than duplicating the filter in multiple test files.
| // Filter helper: extract only ClickHouse query POSTs (exclude KILL QUERY and other control calls) | ||
| function queryPosts(calls) { | ||
| return calls.filter((c) => c.options?.method === 'POST' && c.options?.body?.includes('FORMAT JSON')); | ||
| } | ||
|
|
There was a problem hiding this comment.
This file adds a queryPosts() helper that duplicates the same filtering logic added in js/breakdowns/index.test.js. To avoid divergence, consider factoring this into a shared test helper so both test suites stay aligned when control-plane ClickHouse POSTs (like KILL QUERY) evolve.
| // Cancel any still-running server-side queries from the previous context | ||
| if (ctx.queryGroupPrefix) { | ||
| killQueryGroup(ctx.queryGroupPrefix); | ||
| } | ||
| ctx.controller = new AbortController(); | ||
| ctx.requestId += 1; | ||
| ctx.queryGroupPrefix = `klick-${scope || 'dashboard'}-${ctx.requestId}`; | ||
| setSignalQueryGroup(ctx.controller.signal, ctx.queryGroupPrefix); |
There was a problem hiding this comment.
queryGroupPrefix is derived only from scope + an in-memory incrementing requestId. This can collide across browser tabs/windows (each tab starts at requestId=1), so starting a new context in one tab can KILL QUERY for another tab that happens to share the same scope/prefix. Consider including a per-tab/session random component (e.g., a UUID generated once per page load) in the prefix, and/or avoid embedding scope directly so the prefix is both unique and limited to safe characters for ClickHouse query_id/LIKE matching.
The KILL QUERY command requires SELECT(query_id, user, query) on system.processes. Without this grant, server-side query cancellation silently fails with ACCESS_DENIED. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lars Trieloff <lars@trieloff.net>
AbortSignal.any() returns a new native signal that loses WeakMap associations. The merged signal used in breakdown queries was untagged, so KILL QUERY couldn't cancel them. Explicitly propagate the query group after merging signals in createRequestStatus. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lars Trieloff <lars@trieloff.net>
Summary
query_idprefix, and when a new context replaces the old one, it sendsKILL QUERYto cancel still-running server queriesTesting Done
KILL QUERYworks for read-only users (tested against production ClickHouse Cloud instance)npm test)npm run lint)index.test.jsandapprox-top.test.jsto filter outKILL QUERYPOST calls from assertionsChecklist
npm test)npm run lint)🤖 Generated with Claude Code