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

ClickHouse Failed Dictionary Reload Guard

Find ClickHouse dictionaries failing to load, reload them behind a dry-run and approval gate, then re-read system.dictionaries to prove each one loaded.

Categories
DataInfrastructure

An external dictionary that stops loading does not take ClickHouse down. It sits in system.dictionaries with a last_exception. Every dictGet against it either throws or quietly serves the values from the last successful load. A dashboard shows labels that are weeks old, a join loses the rows added since, nothing surfaces the cause. This guard finds those dictionaries, prints the exception that broke each one, reloads them under a gate, then proves each one came back.

This blueprint was created by zkasuran.

Findings

Measured on ClickHouse 24.8 while building this.

  • A dictionary that loaded once then failed to refresh keeps status = 'LOADED'. Only last_exception changes. So a check written on status != 'LOADED' misses the dangerous case, which is the dictionary that looks healthy while serving stale values. This flow targets last_exception first for that reason.
  • dictGet keeps answering from the last good load, with no warning. Measured: the source table was renamed away, the reload failed, dictGet still returned the old label while the source held a newer one.
  • A successful reload clears last_exception and moves last_successful_update_time, so the re-read is a real gate rather than a hopeful one.

Safety

  • Read-only by default. dry_run is true and auto_remediate is false, so a plain run reports the exceptions without touching anything.
  • A reload 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.
  • The exception for each dictionary is printed before the gate on purpose. A reload re-runs the dictionary source, so approving it means confirming the cause is cleared rather than retrying blind.
  • Each reload is scoped to one named dictionary. Reloads run one at a time, so they do not compete for the same source.
  • The re-read of system.dictionaries is the real gate. A reload that fails again surfaces as a failed run, never as a false success.

How it works

  1. detect (io.kestra.plugin.jdbc.clickhouse.Query) reads system.dictionaries for the database and keeps every dictionary with a non empty last_exception, plus, when include_not_loaded is true, any whose status is not LOADED. It also reads element_count with last_successful_update_time to say whether the dictionary is still serving stale values or nothing at all.
  2. summarize (io.kestra.plugin.core.output.OutputValues) records the targeted names with the per dictionary exception once, so the log, the alert and the failure message all read the same value.
  3. log_detect (io.kestra.plugin.core.log.Log) prints the count, the names, the status of each one plus the exception text that broke it.
  4. guard (io.kestra.plugin.core.flow.If) branches on the targeted count. With none found, clean logs and the run ends.
  5. findings_alert (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the failing dictionaries with their exceptions on every detection, in dry run and in remediate mode.
  6. mode (io.kestra.plugin.core.flow.If) enters the remediation branch only when auto_remediate is true and dry_run is false. Otherwise report_only lists what would be reloaded and stops.
  7. approval_gate (io.kestra.plugin.core.flow.Pause) holds the run until a human resumes it with approved. apply reloads only when approved is true.
  8. remediation_reload_loop (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs SYSTEM RELOAD DICTIONARY <database>.<name> once per targeted dictionary, with allowFailure so one reload that fails again does not stop the pass.
  9. reverify re-reads system.dictionaries for exactly those names. verify_result fails the run through io.kestra.plugin.core.execution.Fail if any of them still carries an exception or is still not LOADED.

What you get

  • A check that reads real dictionary state from system.dictionaries, so it catches the dictionary that is quietly serving values from a load that happened days ago.
  • The exception text per dictionary, in the log and in Slack, so the human approving a reload knows what broke rather than guessing.
  • A reload that is safe to leave scheduled. It reports until an operator turns on remediation and approves the run.
  • A real verification step. The run passes only when the second read shows every targeted dictionary LOADED with no exception.
  • Slack alerts on the findings and on any failure.

Who it's for

  • Data platform engineers who join ClickHouse tables against dictionaries fed from Postgres, MySQL, an HTTP endpoint or a file.
  • Analytics teams who have been bitten by a dashboard showing labels that stopped updating without an error anywhere.
  • DBAs who want the reload step gated on a human reading the exception first, not a cron that retries a broken source forever.

Why orchestrate this with Kestra

ClickHouse records the exception in system.dictionaries then leaves it there. It has no scheduler for this check, no approval step in front of SYSTEM RELOAD DICTIONARY, no way to prove a reload actually succeeded. Kestra adds the missing pieces. It sweeps on a schedule, shows a human the exception before anything is reloaded, gates the reload behind dry-run plus approval, re-reads system.dictionaries to turn a hopeful reload into a verified one, applies the same connection settings to every task then 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, 12 executions covering every path. Two FLAT dictionaries were created over local MergeTree source tables with LIFETIME(MIN 0 MAX 0), both reaching LOADED with dictGet returning values. The source table behind one was then renamed away with SETTINGS check_table_dependencies = 0 and a reload forced, which left it carrying an UNKNOWN_TABLE exception while dictGet kept serving the old label. The second dictionary was left untouched throughout. Every number below was confirmed with clickhouse-client against system.dictionaries.

  • Detector can fail, three ways. The healthy pair returned 0 targeted dictionaries. After the break the same read returned exactly 1, the broken one. A third dictionary pointed at a table that does not exist landed on status = 'FAILED' with element_count = 0 then came back as the only row on the next run.
  • The stale case is real. While the dictionary carried the exception, dictGet returned widget-v3 where the source table held widget-v4, with last_successful_update_time sitting minutes in the past.
  • Dry run detected the failing dictionary, posted the exception text to Slack, changed nothing. element_count, last_successful_update_time plus the exception read identical afterwards.
  • Remediate with approval, source fixed first, ran SYSTEM RELOAD DICTIONARY and the re-read reported 0 remaining. element_count went 2 to 3, last_successful_update_time moved, last_exception cleared, dictGet returned widget-v4 in place of widget-v3.
  • Remediate with approval while the source was still broken reloaded, hit the same UNKNOWN_TABLE exception. The re-read found it still failing so the run FAILED rather than reporting success.
  • A three dictionary reload loop with one unfixable source reloaded the two good ones to LOADED, left the third FAILED, then failed the run naming exactly that one.
  • The approval gate held. A run resumed with approved false reloaded nothing.
  • An unreachable host failed the detect step and fired the Slack alert.
  • Precision. The healthy dictionary was never reloaded, its last_successful_update_time did not move. With include_not_loaded false, three freshly restarted NOT_LOADED dictionaries returned 0, which is correct under lazy loading. Flipping include_not_loaded to true on the same server returned all 3. Scoping the run to one healthy dictionary returned 0 while a broken dictionary sat in the same database.

Prerequisites

A ClickHouse server reachable over the JDBC HTTP interface and a user that can read system.dictionaries and run SYSTEM RELOAD DICTIONARY on the target dictionaries.

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 dictionaries are checked.
dictionary STRING "" Optional single dictionary filter. Empty checks every dictionary.
include_not_loaded BOOL false Also target dictionaries whose status is not LOADED.
dry_run BOOL true Report without reloading.
auto_remediate BOOL false Reload 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 that holds dictionaries. The default dry run reports which ones are failing plus why.
  4. Fix the cause named in the exception, then run again with dry_run false and auto_remediate true. Resume the paused execution with approved true.

How to extend

  • Set include_not_loaded true on a server with eager dictionary loading, where a NOT_LOADED status is itself a fault.
  • Add an element_count floor to the re-read, so a reload that succeeds against an empty source fails the run instead of silently emptying the dictionary.
  • Fail the run as soon as detection finds anything, for a read-only drift check with no remediation.
  • Pair this with a check on the dictionary source itself, so a renamed table or a rotated credential is caught before the next reload is attempted.
  • Probe one dictGet per dictionary after the reload, with the key and attribute for your own schema, to assert the value a dashboard will read.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.