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/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 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 })