Skip to content

Reconnect stale pooled connections via ping_before_query - #8

Merged
tycooon merged 3 commits into
masterfrom
feat/ping-before-query
Jul 20, 2026
Merged

Reconnect stale pooled connections via ping_before_query#8
tycooon merged 3 commits into
masterfrom
feat/ping-before-query

Conversation

@tycooon

@tycooon tycooon commented Jul 20, 2026

Copy link
Copy Markdown
Member

Problem

Long-lived pooled connections get closed out from under the client — ClickHouse's idle_connection_timeout, a load balancer recycling idle sockets, or a server restart. The next use of such a socket hits recv() == 0 and raises ConnectionError: closed: .... (The errno in the message is a stale, meaningless value left in the thread — Success, Operation now in progress, etc. — all the same recv()==0.) The pool's single automatic retry only partially covers this: a burst where several pooled sockets die at once exhausts it.

Change

Enable clickhouse-cpp's built-in connection-resilience options in Client and thread them through Pool, all overridable:

option default effect
ping_before_query true ping and transparently reconnect a dead socket before each query
tcp_keepalive true OS-level TCP keepalive
retry_timeout 1 reconnect backoff in seconds (lowered from clickhouse-cpp's 5s so recovery and give-up stay quick)

The three are exposed as Client accessors. The pool's one-shot ConnectionError retry stays as a backstop for the residual race (socket dies between the ping and the query).

Note on defaults

ping_before_query defaults on, which adds one round-trip per query — this is the behavior change that repairs stale connections out of the box. Set ping_before_query: false to opt out and manage staleness yourself.

Tests

  • defaults / explicit overrides / pool forwarding
  • end-to-end recovery when the server drops an established connection, plus the negative case (raises with ping_before_query: false)

The recovery tests drive an out-of-process TCP proxy that severs live tunnels on demand — Client#initialize connects without releasing the GVL, so an in-process proxy would deadlock.

Long-lived pooled connections get closed out from under the client by
ClickHouse's idle_connection_timeout, a load balancer recycling idle
sockets, or a server restart. The next use of such a socket hit
recv()==0 and raised ConnectionError("closed: ..."), which the pool's
single automatic retry only partially covered (bursts where several
pooled sockets die at once exhausted it).

Enable clickhouse-cpp's connection-resilience options in Client and
thread them through Pool, all overridable:

- ping_before_query (default true): ping and transparently reconnect a
  dead socket before each query, so a stale connection is repaired
  instead of surfacing an error
- tcp_keepalive (default true): OS-level TCP keepalive
- retry_timeout (default 1s): reconnect backoff, lowered from the
  upstream 5s so recovery and give-up stay quick

Expose the three as Client accessors. Add specs for the defaults,
overrides, pool forwarding, and end-to-end recovery when the server
drops the connection (via an out-of-process TCP proxy, since
Client#initialize connects without releasing the GVL).

@tycooon-review-bot tycooon-review-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

Comment thread lib/clickhouse_native/pool.rb
Comment thread spec/spec_helper.rb
…sever

Addresses the Codex review on #8.

Session `settings:` were applied once via a `SET` in the pool's object
factory, so a transparent ping_before_query reconnect — which reuses the
same Client on a fresh socket, bypassing the factory — silently reverted
to server defaults until the client was discarded. Apply them instead as
per-query settings in the binding: they ride every query packet and thus
survive any reconnect. Sent as important, so an unknown setting name still
errors loudly rather than being ignored. Drop the pool's SET path and
settings_sql.

The proxy's sever_all only sent SIGUSR1 without waiting for the child's
trap to close the tunnels, so a follow-up query could race the still-live
socket (making the positive example skip the reconnect and the negative
one flake). Add a pipe the trap writes after closing; sever_all blocks on
it. Add a pool getSetting spec proving settings survive a severed-socket
reconnect.

@tycooon-review-bot tycooon-review-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

Comment thread ext/clickhouse_native/client.cpp
Addresses the Codex re-review of #8.

The per-query settings from the previous commit only rode
execute/query/query_value/query_each. Block inserts go through
Client::Insert, which builds its own INSERT query internally and never
consulted the client's settings — so Pool.new(settings: ...).insert
silently dropped insert-time settings (insert_quorum, max_insert_threads,
...) that the README documents as belonging there.

Add patch 0002-carry-settings-into-insert: it gives clickhouse-cpp's
Client::Insert / Impl::Insert an optional settings parameter, applied as
important per-query settings on the INSERT query it already generates
(reusing all its column logic). The binding passes the client's default
settings through. Update the README insert note and add a regression
test asserting an unknown pool setting makes an insert fail — proving
settings now reach inserts, not just queries.

@tycooon-review-bot tycooon-review-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

@tycooon
tycooon merged commit 38757ae into master Jul 20, 2026
9 checks passed
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