Skip to content

feat(clickhouse sink): add retry_strategy option - #26221

Open
jamesdangercarpenter wants to merge 4 commits into
vectordotdev:masterfrom
jamesdangercarpenter:clickhouse-sink-retry-strategy
Open

feat(clickhouse sink): add retry_strategy option#26221
jamesdangercarpenter wants to merge 4 commits into
vectordotdev:masterfrom
jamesdangercarpenter:clickhouse-sink-retry-strategy

Conversation

@jamesdangercarpenter

@jamesdangercarpenter jamesdangercarpenter commented Aug 26, 2026

Copy link
Copy Markdown

Summary

The clickhouse sink hardcoded ClickhouseRetryLogic::default(), so there was no way to change which HTTP responses it treats as retriable. The http sink already exposes RetryStrategy; this brings the clickhouse sink in line by adding a retry_strategy config option (default unchanged).

Why this matters: a response classified as non-retriable makes the sink drop the batch and finalize it as Rejected. With end-to-end acknowledgements from the kafka source, that is not inert - the source only stores offsets for Delivered batches, but Kafka commits a single watermark per partition, so the next batch that does succeed stores an offset past every rejected batch behind it. The default strategy classes any non-5xx other than 429/408 as non-retriable, which includes the 404 ClickHouse returns for an unknown table.

We hit this during a migration: six consumers started against a ClickHouse service whose schema had not yet been created. Five replayed their full retained backlog in about three minutes, dropped all of it against the missing tables, then committed past the lot the moment the schema appeared and the first batch succeeded. Consumer lag went to zero — which reads as a healthy catch-up — and roughly 1.4M events were lost across four topics with no signal anywhere except the drop counter.

With this option, deployments that must not lose events can retry such responses instead, so the batch never resolves and the watermark cannot advance past it:

retry_strategy:
  type: custom
  status_codes: [401, 403, 404, 408, 429]

The one thing the strategy does not govern is malformed data: ClickHouse reports it as a 500 with a Code: 117 or Code: 53 body, and those stay non-retriable under every strategy - retrying a poison pill would stall ingest indefinitely. Every other response, other 500s included, follows the configured strategy, so none and a custom list that excludes 500 are honoured there too.

Changes:

  • src/sinks/util/http.rs: add HttpRetryLogic::new(RetryStrategy) for sinks that surface retry_strategy in their own configuration.
  • src/sinks/clickhouse/{config,service}.rs: add the retry_strategy option and thread it into ClickhouseRetryLogic, keeping the malformed-data (Code 117/53) exception on top of whatever strategy is configured. ClickhouseRetryLogic forwards is_retriable_timeout to the inner HttpRetryLogic so none also suppresses timeout retries.
  • Generated cue docs and changelog fragment.

References

Vector configuration

sources:
  kafka:
    type: kafka
    bootstrap_servers: "broker:9092"
    topics: ["events"]
    group_id: vector
    acknowledgements:
      enabled: true

sinks:
  clickhouse:
    type: clickhouse
    inputs: ["kafka"]
    endpoint: "http://clickhouse:8123"
    table: events
    retry_strategy:
      type: custom
      status_codes: [401, 403, 404, 408, 429]

How did you test this PR?

  • Unit tests cover both directions plus the 500 range: the custom strategy retries an unknown-table 404, the default strategy keeps retrying other server errors, none and a custom list without 500 leave a 500 non-retriable, is_retriable_timeout tracks the strategy, and malformed-data 500s (Code: 117/Code: 53) stay non-retriable under every strategy (cargo test --lib sinks::clickhouse, 46 passed).
  • cargo check, cargo fmt --check, clippy clean on the changed files.
  • Regenerated component docs with make generate-component-docs.
  • The change has been running in production on our fork, where the custom strategy held offsets in place during a repeat of the missing-schema scenario.

Is this a breaking change?

  • Yes
  • No

Does this PR include user facing changes?

  • Yes. Please add a changelog fragment based on our guidelines.
  • No. A maintainer will apply the no-changelog label to this PR.

The clickhouse sink hardcoded ClickhouseRetryLogic::default(), so there was
no way to change which HTTP responses it treats as retriable. The http sink
already exposes RetryStrategy; this brings the clickhouse sink in line.

The default strategy classes any non-5xx other than 429/408 as
non-retriable, which includes the 404 ClickHouse returns for an unknown
table. That interacts badly with end-to-end acknowledgements on the kafka
source. A non-retriable response drops the batch and finalizes it as
Rejected; the source only calls store_offset for Delivered batches, so a
rejected batch does not advance the offset by itself. But Kafka commits a
single watermark per partition, so the next batch that *does* succeed
stores an offset past every rejected batch behind it.

Observed in a dev migration: six shadow consumers started against a
ClickHouse service whose schema had not been created yet. Five of them
replayed their full retained backlog in about three minutes, dropped all of
it against the missing tables, and then committed past the lot the moment
the schema appeared and the first batch succeeded. Consumer lag went to
zero, which reads as a healthy catch-up. Roughly 1.4M events were lost
across four topics with no error surviving anywhere except the drop counter.

Retrying instead means the batch never resolves, OrderedFinalizer stalls
behind it, and the watermark cannot move:

  retry_strategy:
    type: custom
    status_codes: [401, 403, 404, 408, 429]

The 500 handling is deliberately untouched. ClickHouse reports malformed
data as a 500 with a `Code: 117` or `Code: 53` body, and those stay
non-retriable under every strategy — retrying a poison pill would wedge
every batch queued behind it. Tests cover both directions plus the carve-out.

Note that this does not make loss impossible: a batch that legitimately
cannot be retried still advances the watermark past itself when the next one
succeeds. component_discarded_events_total is the only signal for that, and
it deserves an alert rather than a dashboard panel.
@jamesdangercarpenter
jamesdangercarpenter requested review from a team as code owners August 26, 2026 02:04
@github-actions github-actions Bot added domain: sinks Anything related to the Vector's sinks domain: external docs Anything related to Vector's external, public documentation docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. labels Aug 26, 2026
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eedd51d482

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sinks/clickhouse/service.rs
Comment thread src/sinks/clickhouse/service.rs
@jamesdangercarpenter

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Two gaps in the previous commit, both reported in review.

should_retry_response hardcoded RetryAction::Retry for any 500 whose body
was not Code: 117 or Code: 53, so a strategy of `none`, or a `custom` list
without 500, was ignored for exactly the status ClickHouse uses most. With
unlimited retries by default that stalls ingest on a response the operator
explicitly asked not to retry. Only the malformed-data exception overrides
the strategy now; every other response, 500s included, is delegated.

ClickhouseRetryLogic also did not implement is_retriable_timeout, so it
inherited the RetryLogic default of true instead of forwarding to the inner
HttpRetryLogic, which returns false for `none`. That contradicted the
documented promise of `none` not to retry timeouts, and risks duplicate
inserts after an ambiguous timeout. It forwards now.

Tests cover both: a 500 stays non-retriable under `none` and under a custom
list that excludes it, and is_retriable_timeout tracks the strategy.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0715f8abe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sinks/clickhouse/config.rs Outdated
Now that ordinary 500s follow the configured strategy, the documented
custom example ([401, 403, 404, 408, 429]) would disable the default
5xx retries and drop batches on transient server errors — the opposite
of its loss-avoidance intent. Recommend `type: all` instead, and note
that a custom list must include the server errors the default would
have retried.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: external docs Anything related to Vector's external, public documentation domain: sinks Anything related to the Vector's sinks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant