Schedule icon
Query icon
Get icon
OutputValues icon
Set icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Fail icon

ClickHouse Detached Parts Cleanup Guard

Detect debris in ClickHouse system.detached_parts, drop stale parts behind a grace window and approval gate, then re-read to confirm.

Categories
DataInfrastructure

Failed merges, failed mutations, broken parts and manual DETACH leave data parts in a ClickHouse table's detached directory. They show up in system.detached_parts, they never rejoin a table on their own, they are not queried and they waste disk until someone removes them. This guard finds the debris, drops the stale parts under a gate and proves the drop landed.

This blueprint was created by zkasuran.

Safety

  • Read-only by default. dry_run is true and auto_remediate is false, so a plain run reports and never drops.
  • A drop happens only when auto_remediate is true and dry_run is false. It then waits for a human to resume the Pause with approved set true. An unanswered gate reads as not approved.
  • A first-seen grace window protects a fresh detach. A part is dropped only once this guard has seen it in system.detached_parts for at least staleness_minutes, so a part detached moments ago, which an operator may be about to ATTACH, is reported but never dropped on the same run.
  • A plain manual detach (an empty reason) is left alone by default, because it may be intentional. Set include_manual_detach to also target it.
  • Each drop is scoped by database, table and the full detached name and runs one statement at a time. The re-read of system.detached_parts is the real gate, so a drop the user is not allowed to run surfaces as a failed run rather than a false success.

How it works

  1. now_probe (io.kestra.plugin.jdbc.clickhouse.Query) reads the server clock as epoch seconds, so the first-seen age is measured against the ClickHouse clock.
  2. detect (io.kestra.plugin.jdbc.clickhouse.Query) reads system.detached_parts for the database and keeps the debris reasons from target_reasons, plus empty-reason manual detaches when include_manual_detach is true.
  3. read_cursor (io.kestra.plugin.core.kv.Get) loads the first-seen cursor, a KV map from database::table::name to the epoch second this guard first saw the part. A missing key is the first run.
  4. compose and enrich (io.kestra.plugin.core.output.OutputValues) merge the cursor with the detected parts, set a first-seen time of now for a part seen for the first time, compute each part's age and flag a part a target once its age is at or past staleness_minutes.
  5. plan (io.kestra.plugin.core.output.OutputValues) splits the parts into drop targets and parts still inside the grace window, then builds the pruned cursor.
  6. write_cursor (io.kestra.plugin.core.kv.Set) writes the pruned cursor back with no ttl, so a long-lived part keeps its original first-seen time and a part that is gone drops out of the cursor.
  7. guard (io.kestra.plugin.core.flow.If) branches on the debris count. With none found, clean logs and the run ends. Otherwise findings_alert posts the breach on every detection.
  8. mode (io.kestra.plugin.core.flow.If) enters the remediation branch only when auto_remediate is true, dry_run is false and at least one part is past the grace window. Otherwise report_only lists what would be dropped and stops.
  9. approval_gate (io.kestra.plugin.core.flow.Pause) holds the run until a human resumes it. drop_detached_parts (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) then runs ALTER TABLE ... DROP DETACHED PART ... SETTINGS allow_drop_detached = 1 once per target.
  10. reverify re-reads system.detached_parts and verify_result fails the run through io.kestra.plugin.core.execution.Fail if any targeted part is still present. A fresh part left in place by design does not count.

What you get

  • A check that reads the real debris from system.detached_parts, so it catches the parts that waste disk forever and never rejoin a table.
  • A grace window that holds back a drop until the guard has seen a part for a set time, so a detach an operator is about to reverse is reported but kept.
  • A drop that is safe to leave scheduled. It reports until an operator turns on remediation and approves each run.
  • A real verification step. The run passes only when the second read of system.detached_parts shows every target gone.
  • Slack alerts on the findings and on any failure.

Who it's for

  • Data platform engineers who run ClickHouse and watch disk fill with detached debris from failed merges and mutations.
  • DBAs who need to clear broken and unexpected parts without hand-written SQL and without touching a part someone meant to re-attach.
  • Teams evaluating the ClickHouse plugin who want an example that reads a system table, keeps a KV cursor and runs gated, verified DDL.

Why orchestrate this with Kestra

ClickHouse has no scheduler, no grace window, no approval step and no way to prove a cleanup command took effect. Kestra adds the missing pieces. It schedules the sweep, keeps a first-seen cursor in KV so a fresh detach is held back, gates the destructive statement behind dry-run and approval, re-reads system.detached_parts to turn a hopeful drop into a verified one, applies the same connection settings to every task and posts a Slack alert the moment a step fails.

Tested end to end

Run on Kestra 2.0.5 against a ClickHouse 24.8 server. A MergeTree table was seeded with five parts, then four were detached. One kept an empty reason (manual). Three were turned into debris with broken, unexpected and ignored reasons, confirmed in system.detached_parts with clickhouse-client.

  • Detector can fail: a clean table returned 0 debris parts, the seeded table returned 3 (confirmed with clickhouse-client).
  • Dry run detected the 3 debris parts, alerted and left all of them in place.
  • Remediate with approval and an aged cursor dropped the 3 targets and the re-read reported 0 of them remaining (3 before, 0 after in system.detached_parts).
  • Grace window: a part seen for the first time and a manual detach with the default settings were reported but not dropped.
  • A read-only user turned every drop into a no-op, the re-read found the targets still present and the run failed and alerted.
  • An unreachable host failed the first query and fired the Slack alert.

Prerequisites

A ClickHouse server reachable over the JDBC HTTP interface and a user that can read system.detached_parts and run ALTER TABLE ... DROP DETACHED PART on the target tables.

Secrets

  • CLICKHOUSE_USERNAME: ClickHouse user, read when use_secret_auth is true.
  • CLICKHOUSE_PASSWORD: password for that user.
  • SLACK_WEBHOOK_URL: used by findings_alert and the errors block.
  • In Kestra OSS, provide each secret as a base64-encoded environment variable prefixed SECRET_.

Inputs

Name Type Default Description
clickhouse_url STRING jdbc:clickhouse://localhost:8123/default ClickHouse JDBC URL.
use_secret_auth BOOL true Read credentials from secrets. Set false only for inline test credentials.
database STRING default Database whose detached parts are checked.
table STRING "" Optional single table filter. Empty checks every table.
target_reasons STRING broken,covered-by-broken,unexpected,ignored,clone Detach reasons that count as debris.
include_manual_detach BOOL false Also target empty-reason manual detaches.
staleness_minutes INT 60 Grace window. A part is dropped only after it has been seen this long.
dry_run BOOL true Report without dropping.
auto_remediate BOOL false Drop behind the approval gate when dry_run is false.

Quick start

  1. Set CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD and SLACK_WEBHOOK_URL as namespace secrets.
  2. Import this flow.
  3. Run it with database pointed at a ClickHouse database. The default dry run reports the stale detached parts.
  4. To drop, run again with dry_run false and auto_remediate true, then resume the paused execution with approved true.

How to extend

  • Raise or lower staleness_minutes to widen or shorten the grace window before a part is eligible to drop.
  • Set include_manual_detach true to also clear plain manual detaches once they age past the window.
  • Replace the drop with ALTER TABLE ... ATTACH PART for a flow that re-attaches a specific recovered part instead of removing it.
  • Fail the run as soon as detection finds anything, for a read-only drift check with no remediation.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.