Skip to content

feat(webapp): isolate the runs list ClickHouse read pool - #4763

Open
ericallam wants to merge 7 commits into
mainfrom
feature/tri-13470-runs-list-clickhouse-one-tenants-expensive-queries-can
Open

feat(webapp): isolate the runs list ClickHouse read pool#4763
ericallam wants to merge 7 commits into
mainfrom
feature/tri-13470-runs-list-clickhouse-one-tenants-expensive-queries-can

Conversation

@ericallam

@ericallam ericallam commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

Improves the performance and reliability of the runs list and the runs.list API, especially for large projects and filtered views.

What changed

  • Filtered runs-list queries use PREWHERE. Immutable and additive-only filters (tags, task identifier, version, queue, region, machine, and the rest) are applied in PREWHERE on the task_runs_v2 FINAL scan, so ClickHouse filters, and uses the tags skip index, before it reconciles versions and materialises the wide columns. Same results, far less memory per query. status stays in WHERE: it changes across a run's versions, so filtering it before FINAL could return stale rows.
  • The runs-list ClickHouse pool gets per-query guardrails, all env-configurable: a max_execution_time paired with the client request timeout, a per-query max_memory_usage, a max_threads cap, and readonly. Each bounds a single query to itself, so a heavy query can't affect other queries, and they are safe as pool-level settings only because this pool is read-only.
  • Billing and bulk count reads move to the read pool, off the write pool.

Defaults are conservative for self-hosters; production values are set via env.

Give the runs-list ClickHouse pool server-side query protection
(max_execution_time, thread and memory caps, a per-user concurrency
breaker, readonly) so one tenant expensive query cannot saturate the
shared read service, and cap the runs list created_at lower bound to a
bounded window so an unbounded filter cannot scan every partition.

Billing and bulk count reads move to the read pool, off the ingestion
writer. Count queries are never date-capped so billing keeps counting
runs of any age.
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: fcd9a8d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The runs list now uses a dedicated ClickHouse configuration with query limits, resource caps, readonly mode, and request timeouts. Runs-list filters use PREWHERE for immutable fields while lifecycle and date filters remain in WHERE. The created-at age cap was removed. Bulk actions, billing-limit queries, and bulk-action creation now select the runsList client. New tests cover ClickHouse protections and query behavior.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the technical changes but omits the required issue reference, checklist, testing, changelog, and screenshots sections. Add the required template sections, including the issue reference, completed checklist, testing steps, changelog entry, and screenshots or an explicit not-applicable note.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: isolating the runs-list ClickHouse read pool.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tri-13470-runs-list-clickhouse-one-tenants-expensive-queries-can

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

… instead of a created_at clamp

Replaces the created_at window clamp with PREWHERE routing on the runs-list
query. Immutable and additive-only filters (tags, task_identifier, and the rest)
move into PREWHERE so ClickHouse filters, and uses the tags skip index, before
FINAL reconciles versions and before materialising the wide columns. This bounds
the memory a filtered runs-list query uses without dropping any rows, unlike the
date clamp which hid older runs from the list and the runs.list API.

status stays in WHERE (post-FINAL): it is the one lifecycle-mutable filter, so
PREWHERE-ing it would keep a stale version and drop the winning one.
coderabbitai[bot]

This comment was marked as resolved.

Drops max_memory_usage_for_user and max_concurrent_queries_for_user. Those are
per-ClickHouse-user limits, and every connection is the default user, so hitting
the shared budget rejects whichever query arrives next rather than the one
responsible, which would fail queries for uninvolved tenants. The per-query caps
(max_execution_time, max_memory_usage, max_threads) bound a bad query to itself,
and the server-level max_server_memory_usage protects the node.
max_threads and max_memory_usage were opt-in env vars, so out of the box, or if
deploy config lagged, the pool ran with no thread or per-query memory cap, which
is the thread oversubscription that hurt throughput under load. Give both a
conservative default (4 threads, 1 GiB) so the guardrails hold without depending
on a deploy-time config step, following the logs and query read pools.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts (1)

312-314: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make the runs-list resource caps non-overridable.

readonly=2 permits session setting changes. The ClickHouse wrapper merges per-operation clickhouse_settings after client-level settings, so callers can set max_memory_usage=0 or raise max_threads and max_execution_time. Enforce these caps with immutable server-side constraints for both default and organization-specific runsList clients.

🧹 Nitpick comments (1)
apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts (1)

306-322: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add crumb instrumentation for the new settings path.

Add an approved // @Crumbs marker or `// `#region` `@crumbs block around getRunsListClickhouseSettings during development. Strip it before merge.

As per coding guidelines, add crumbs as you write code, not only when debugging, using // @Crumbs or `// `#region` `@crumbs.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 640a8da7-abcd-4ef2-98d9-1a8f181d42c1

📥 Commits

Reviewing files that changed from the base of the PR and between d2bbe1b and 7f8db90.

📒 Files selected for processing (2)
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
💤 Files with no reviewable changes (1)
  • apps/webapp/app/env.server.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (33)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (9)
Never use `request.signal` to detect client disconnects. Use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts`, which is wired to Express response close events.

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
Use zod for validation in packages/core and apps/webapp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
Access environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env`

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
🔇 Additional comments (2)
apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts (2)

1-1: LGTM!


352-353: 🩺 Stability & Availability

No change is required for query cancellation.

@internal/clickhouse already sets cancel_http_readonly_queries_on_client_close: 1 for the runs-list client.

			> Likely an incorrect or invalid review comment.

coderabbitai[bot]

This comment was marked as resolved.

@ericallam
ericallam marked this pull request as ready for review August 24, 2026 21:55
devin-ai-integration[bot]

This comment was marked as resolved.

Client request timeout now sits above the server max_execution_time (default
40s vs 35s, and the factory forces it to at least exec + 5s), so the server-side
cap is what stops a slow query and the client stays connected to receive the
error, instead of aborting first and leaving the query running. The numeric caps
reject zero and negative values, since ClickHouse treats 0 as unlimited for
max_execution_time and max_memory_usage, which would silently disable them.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
apps/webapp/app/env.server.ts (1)

2236-2243: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add required crumbs to the new runs-list protection code.

  • apps/webapp/app/env.server.ts#L2236-L2243: mark the new environment settings with // @Crumbs or `// `#region` `@crumbs.
  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts#L1-L1: cover the new import with a crumb marker.
  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts#L306-L331: mark the timeout helper and settings block.
  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts#L361-L363: mark default runs-list client wiring.
  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts#L607-L609: mark organization-specific runs-list client wiring.

As per coding guidelines, add crumbs as you write code and mark lines with // @Crumbs or `// `#region` `@crumbs.

Source: Coding guidelines

apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts (1)

306-317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the derived timeout through the factory.

The supplied apps/webapp/test/runsListClickhouseSettings.test.ts constructs ClickHouse directly with requestTimeoutMs: 30_000. It does not verify getRunsListRequestTimeoutMs() or the "runsList" organization-client branch. Add tests for configured timeouts below and above (max_execution_time + 5) * 1000.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c598e0b-f651-4465-9fe6-7a1867b827e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1592856 and 3d7ef3b.

📒 Files selected for processing (2)
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (36)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: typecheck / typecheck
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: code-quality / code-quality
  • GitHub Check: audit
  • GitHub Check: audit
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (9)
Never use `request.signal` to detect client disconnects. Use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts`, which is wired to Express response close events.

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
Use zod for validation in packages/core and apps/webapp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
Access environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env`

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
  • apps/webapp/app/env.server.ts

devin-ai-integration[bot]

This comment was marked as resolved.

The region filter uses if(region != "", region, worker_queue): a run is
region="" at trigger (so the expression yields worker_queue) and gets a real
region at dequeue, so the expression flips from one non-empty value to another
across a run versions. Under PREWHERE that is evaluated before FINAL reconciles
versions, so it could keep a stale pre-dequeue version and drop the winner,
returning runs whose current region no longer matches (and listRunIds drives
bulk actions). Moved it back to WHERE (post-FINAL) with a regression test.
devin-ai-integration[bot]

This comment was marked as resolved.

Both reflect execution/outcome and change across a run versions, so they are
unsafe in PREWHERE (evaluated before FINAL): error_fingerprint is derived from
status per snapshot and is cleared when a run recovers to a non-error status, and
machine_preset can escalate to a larger machine on an out-of-memory retry. In
either case an earlier version matches the filter while the winning version does
not, so PREWHERE could keep the stale version and drop the winner. Only
trigger-time identity columns and append-only arrays stay in PREWHERE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant