Skip to content

feat(clickhouse): add partition attachment tooling - #8357

Merged
onewland merged 5 commits into
masterfrom
oliver/partition-management
Sep 1, 2026
Merged

feat(clickhouse): add partition attachment tooling#8357
onewland merged 5 commits into
masterfrom
oliver/partition-management

Conversation

@onewland

@onewland onewland commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a reusable partition-management module for listing and attaching ClickHouse partitions
  • attach partitions serially with a health check between operations and dry-run support
  • add a headless snuba attach-partitions command with optional --partition-id targeting
  • skip already-active destination partition IDs during discovery, while direct partition attachment bypasses discovery
  • allow a custom, partition-scoped health check query via --health-check-query

Health check

--health-check-query
lets an operator supply a custom gate:

snuba attach-partitions eap_items_src eap_items_dst --storage eap_items --execute \
  --health-check-query "SELECT count() < 1000000 FROM eap_items_dst
                        WHERE timestamp >= %(partition_start)s
                        AND retention_days = %(retention_days)s"

The query runs before every attach, gating the first operation and each one
following an attach. A failure aborts immediately, so a bad destination stops
the run rather than absorbing every remaining partition. Unhealthy means the
query raised, returned no rows, or returned a falsy first value.

Bound parameters

Two parameters are substituted so the check can scope to the partition about
to be attached:

  • %(partition_start)s — start of the partition's time boundary
  • %(retention_days)s — bound when the partition key carries one

Both are needed for EAP tables. eap_items partitions by
(retention_days, toMonday(timestamp)), so 30-20240212 and 90-20240212
share the boundary 2024-02-12. A check scoped only by time cannot tell them
apart: it reads rows from a partition that was already attached, and reports a
false unhealthy on a healthy destination. Scoping on retention_days as well
identifies a single partition.

Referencing a parameter the partition does not carry (for example
%(retention_days)s on a date-only key) fails before the query runs, listing
what is available, so a stray placeholder cannot quietly pass every check.

Risk

Low. There is currently no entrypoint into this code path:

  • nothing in the codebase imports snuba.clickhouse.partition_management
    outside its own tests
  • snuba attach-partitions is picked up by CLI auto-discovery but is not
    referenced by any GoCD pipeline, GitHub workflow, Makefile target, cron, or
    manual job, so it only runs when a human invokes it
  • the default remains SELECT 1, so --health-check-query is opt-in
  • the command defaults to --dry-run; --execute is required to write
  • ATTACH PARTITION ... FROM leaves the source intact, and active destination
    partition IDs are skipped, so re-running is safe

Testing

Verified end to end against a local ClickHouse using tables with the real
(retention_days, toMonday(timestamp)) key, plus a String-key table with
hashed partition IDs: boundaries derived correctly in both, all partitions
attached, re-run was a no-op, and an unhealthy query aborted before any attach
with the destination left empty.

onewland and others added 3 commits August 19, 2026 16:56
The health check for partition attachment was a fixed SELECT 1, so it
could only tell whether ClickHouse was reachable. Allow an operator to
supply a query instead, and bind the partition being attached into it.

Derive the boundary from system.parts.partition rather than the
partition ID. A tuple key such as (retention_days, toMonday(timestamp))
renders as (90,'2024-02-12'), which exposes the boundary even when the
partition ID is hashed, as it is once a String column joins the key.

Bind retention_days alongside the boundary. Partitions that differ only
by retention share a boundary, so partition_start alone cannot identify
one of them, and a check scoped only by time reads rows belonging to a
partition that was already attached.

Fail before running a query that references a parameter the partition
does not carry, so a stray placeholder cannot quietly pass every check.
@onewland
onewland marked this pull request as ready for review September 1, 2026 18:44
@onewland
onewland requested a review from a team as a code owner September 1, 2026 18:44

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 09e204c. Configure here.

Comment thread snuba/cli/attach_partitions.py
StorageKey accepts any string without validating it, so an unknown
storage reaches the registry and fails on a dict lookup. That raises
KeyError, which the handler did not catch, so a typo in --storage exited
with a traceback instead of the intended message.

Reported by Cursor Bugbot.
Comment thread snuba/cli/attach_partitions.py Outdated
Comment on lines +77 to +85
from snuba.clickhouse.partition_management import (
PartitionBoundaryError,
attach_partition_from_table,
attach_partitions_from_table,
build_health_check,
get_partition_boundaries,
)
from snuba.clickhouse.pool import ClickhousePool
from snuba.clusters.cluster import ClickhouseNode, build_pool

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.

can we move this to the top of the file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 2347ef8. Moved all three (partition_management, ClickhousePool, ClickhouseNode/build_pool) to the top, merging the cluster names into the existing import.

Safe to hoist here: SnubaCLI.get_command calls initialize_snuba() before compiling the command module, and the file already imported from snuba.clusters.cluster and snuba.datasets.storages.factory at module level. Verified --help, a live dry run, and snuba --help for the other commands, so no import cycle.

Comment thread snuba/cli/attach_partitions.py
The command imported its dependencies inside the function body. The CLI
loader initializes snuba before compiling command code, so a module
level import is already safe here, and the top-level import block
covered the same packages.
Comment on lines +149 to +159
database,
source_table,
destination_table,
health_check_query=health_check_query,
dry_run=not execute,
on_partition_attached=lambda attached_partition_id: click.echo(
f"Attached partition {attached_partition_id}"
),
)
except PartitionBoundaryError as error:
raise click.UsageError(str(error)) from error

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.

Bug: The attach-partitions CLI command doesn't handle UnhealthyError, causing a full traceback to be displayed to the user when a health check fails during execution.
Severity: MEDIUM

Suggested Fix

Update the try/except block in snuba/cli/attach_partitions.py to also catch UnhealthyError. Convert the caught exception into a click.UsageError or click.ClickException to provide a clean error message to the user, following the existing pattern for CLI error handling in the codebase.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: snuba/cli/attach_partitions.py#L128-L159

Potential issue: The `try/except` block in the `attach-partitions` CLI command is
designed to catch errors during partition attachment. However, it only catches
`PartitionBoundaryError`. When a health check fails, the `run_health_check_query`
function raises an `UnhealthyError`. This exception is not caught by the CLI's exception
handler, causing a full Python traceback to be displayed to the user instead of a clean,
user-friendly error message. This can happen when running the command with the
`--execute` flag and a health check fails.

@onewland
onewland enabled auto-merge (squash) September 1, 2026 19:48
@onewland
onewland merged commit f8c3b24 into master Sep 1, 2026
66 checks passed
@onewland
onewland deleted the oliver/partition-management branch September 1, 2026 20:04
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.

2 participants