From bf66adc86b104ff82793a89a4aa64e81e0a03ecc Mon Sep 17 00:00:00 2001 From: Yuri Smirnov Date: Wed, 29 Apr 2026 12:48:44 +0300 Subject: [PATCH 1/2] Retry once on stale-connection ConnectionError in Pool#with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pool checkouts can hand out connections whose server-side end has been closed while idle (CH idle_connection_timeout / tcp_keep_alive_timeout, LB / firewall idle drops, server restart). The first read on such a socket sees recv() == 0, which clickhouse-cpp surfaces as std::system_error(errno=0, "closed") and we wrap as ClickhouseNative::ConnectionError("closed: Success"). The pool already discards the dead client (`discard_current_connection` in the rescue) so the next checkout is fresh — but we used to re-raise the error to the caller, who'd see a noisy "closed: Success" failure on the very first operation after their pooled client went stale. Now Pool#with discards-and-retries once on ConnectionError. If the retry also fails (e.g. server actually down), we raise. Other error classes (ServerError, ProtocolError, EncoderError, etc.) are still not retried — those are real and re-running could mask logic bugs or, for ServerError on a half-completed write, double-execute. Three new specs cover the path: success on retry, raise after the second failure, and no retry on non-ConnectionError. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/clickhouse_native/pool.rb | 23 ++++++++++++++++++---- spec/clickhouse_native_spec.rb | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/lib/clickhouse_native/pool.rb b/lib/clickhouse_native/pool.rb index 602eb55..9ab2a51 100644 --- a/lib/clickhouse_native/pool.rb +++ b/lib/clickhouse_native/pool.rb @@ -29,11 +29,26 @@ def initialize(host:, port:, database: "default", user: "default", password: "", # session settings), producing misleading log lines and re-raises in # unrelated code. A fresh socket + handshake is cheap relative to # debugging that. + # + # ConnectionError gets one automatic retry: pooled connections that + # have been idle long enough for the server / an LB to FIN them + # surface as "closed" on the very next recv (errno 0, message + # "closed: Success"). Discarding and re-checking out lands a fresh + # socket and the operation succeeds. The retry only triggers when + # the dead-connection error fired before any data was sent, so + # write operations don't risk double-execution from this path. def with - @pool.with do |client| - yield client - rescue - @pool.discard_current_connection(&:close) + attempts = 0 + begin + @pool.with do |client| + yield client + rescue + @pool.discard_current_connection(&:close) + raise + end + rescue ConnectionError + attempts += 1 + retry if attempts == 1 raise end end diff --git a/spec/clickhouse_native_spec.rb b/spec/clickhouse_native_spec.rb index 6e31cdb..586522f 100644 --- a/spec/clickhouse_native_spec.rb +++ b/spec/clickhouse_native_spec.rb @@ -477,6 +477,41 @@ expect(pool.database).to eq("default") end + describe "ConnectionError retry" do + it "retries once after a stale-connection ConnectionError" do + attempts = 0 + result = pool.with do |client| + attempts += 1 + raise ClickhouseNative::ConnectionError, "closed: Success" if attempts == 1 + client.query_value("SELECT 1") + end + expect(result).to eq(1) + expect(attempts).to eq(2) + end + + it "raises after the second ConnectionError" do + attempts = 0 + expect do + pool.with do + attempts += 1 + raise ClickhouseNative::ConnectionError, "closed: Success" + end + end.to raise_error(ClickhouseNative::ConnectionError) + expect(attempts).to eq(2) + end + + it "does not retry non-connection errors" do + attempts = 0 + expect do + pool.with do + attempts += 1 + raise ClickhouseNative::ServerError, "boom" + end + end.to raise_error(ClickhouseNative::ServerError) + expect(attempts).to eq(1) + end + end + describe "settings:" do it "applies Integer settings to every client on checkout" do p = described_class.new(**CH_KWARGS, pool_size: 2, settings: { max_threads: 7 }) From b82f91b27639570090cce561bd3324701dfd24ec Mon Sep 17 00:00:00 2001 From: Yuri Smirnov Date: Wed, 29 Apr 2026 12:52:45 +0300 Subject: [PATCH 2/2] Bump version 0.7.0 -> 0.8.0 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/clickhouse_native/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/clickhouse_native/version.rb b/lib/clickhouse_native/version.rb index 7ae34ac..e5d17a0 100644 --- a/lib/clickhouse_native/version.rb +++ b/lib/clickhouse_native/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ClickhouseNative - VERSION = "0.7.0" + VERSION = "0.8.0" end