From 86447efcaf5a5d199ff02ddff48f8114d6f7edd5 Mon Sep 17 00:00:00 2001 From: John Rhoads Date: Wed, 16 Sep 2026 12:08:18 -0600 Subject: [PATCH 1/4] Use TinyTDS Client#ping in active? when available Idle RST leaves FreeTDS dbdead/active? stale-true until I/O fails. When TinyTDS provides Client#ping (rails-sqlserver/tiny_tds#609), call it from active? with a short FreeTDS-bounded timeout (config :ping_timeout, default 2s). Older TinyTDS keeps the passive active? path. Related to activerecord-sqlserver-adapter#1396. Co-authored-by: Cursor --- CHANGELOG.md | 1 + .../connection_adapters/sqlserver_adapter.rb | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ede15d0..e03ce729e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Added - [#1385](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1385) Added support for Nulls first and last. +- Use TinyTDS `Client#ping(timeout:)` from `active?` when available so idle RST / half-dead checkouts are detected before the next statement (see [tiny_tds#609](https://github.com/rails-sqlserver/tiny_tds/pull/609), [#1396](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/1396)). Falls back to passive `active?` on older TinyTDS. Configure with `:ping_timeout` (seconds, default 2). #### Changed diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 03f542bd5..0e72d2e11 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -50,6 +50,11 @@ class SQLServerAdapter < AbstractAdapter # Default precision for 'time' (See https://docs.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql) DEFAULT_TIME_PRECISION = 7 + # FreeTDS-bounded checkout ping timeout (seconds) when TinyTDS provides + # Client#ping. Override with config :ping_timeout. + # See rails-sqlserver/tiny_tds#609 and activerecord-sqlserver-adapter#1396. + DEFAULT_PING_TIMEOUT = 2 + attr_reader :spid cattr_accessor :cs_equality_operator, instance_accessor: false @@ -143,6 +148,7 @@ def initialize(...) @config[:appname] = self.class.rails_application_name unless @config[:appname] @config[:login_timeout] = @config[:login_timeout].present? ? @config[:login_timeout].to_i : nil @config[:timeout] = @config[:timeout].present? ? @config[:timeout].to_i / 1000 : nil + @config[:ping_timeout] = @config[:ping_timeout].present? ? @config[:ping_timeout].to_i : DEFAULT_PING_TIMEOUT @config[:encoding] = @config[:encoding].present? ? @config[:encoding] : nil @connection_parameters ||= @config @@ -285,7 +291,14 @@ def disable_referential_integrity # === Abstract Adapter (Connection Management) ================== # def active? - if @raw_connection&.active? + return false unless @raw_connection + + if raw_connection_pingable? + return false unless @raw_connection.ping(timeout: ping_timeout_seconds) + + verified! + true + elsif @raw_connection.active? verified! true end @@ -518,6 +531,16 @@ def connection_errors end end + def raw_connection_pingable? + @raw_connection.respond_to?(:ping) + end + + def ping_timeout_seconds + seconds = @config[:ping_timeout] + seconds = DEFAULT_PING_TIMEOUT if seconds.nil? || seconds.to_i <= 0 + seconds.to_i + end + def initialize_dateformatter @database_dateformat = user_options_dateformat a, b, c = @database_dateformat.each_char.to_a From bb62c7d5bfe93b695bb187096b8afcc6d1ffff14 Mon Sep 17 00:00:00 2001 From: John Rhoads Date: Wed, 16 Sep 2026 12:09:57 -0600 Subject: [PATCH 2/4] Allow ping_timeout: 0 to disable TinyTDS checkout ping Explicit zero must not fall through to the default; use the passive active? path when ping is disabled or unavailable. Co-authored-by: Cursor --- CHANGELOG.md | 2 +- .../connection_adapters/sqlserver_adapter.rb | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e03ce729e..2a27f8fa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ #### Added - [#1385](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1385) Added support for Nulls first and last. -- Use TinyTDS `Client#ping(timeout:)` from `active?` when available so idle RST / half-dead checkouts are detected before the next statement (see [tiny_tds#609](https://github.com/rails-sqlserver/tiny_tds/pull/609), [#1396](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/1396)). Falls back to passive `active?` on older TinyTDS. Configure with `:ping_timeout` (seconds, default 2). +- Use TinyTDS `Client#ping(timeout:)` from `active?` when available so idle RST / half-dead checkouts are detected before the next statement (see [tiny_tds#609](https://github.com/rails-sqlserver/tiny_tds/pull/609), [#1396](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/1396)). Falls back to passive `active?` on older TinyTDS. Configure with `:ping_timeout` (seconds, default 2; `0` disables ping). #### Changed diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 0e72d2e11..b4f1eb0f3 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -51,7 +51,7 @@ class SQLServerAdapter < AbstractAdapter DEFAULT_TIME_PRECISION = 7 # FreeTDS-bounded checkout ping timeout (seconds) when TinyTDS provides - # Client#ping. Override with config :ping_timeout. + # Client#ping. Override with config :ping_timeout (0 disables ping). # See rails-sqlserver/tiny_tds#609 and activerecord-sqlserver-adapter#1396. DEFAULT_PING_TIMEOUT = 2 @@ -148,7 +148,13 @@ def initialize(...) @config[:appname] = self.class.rails_application_name unless @config[:appname] @config[:login_timeout] = @config[:login_timeout].present? ? @config[:login_timeout].to_i : nil @config[:timeout] = @config[:timeout].present? ? @config[:timeout].to_i / 1000 : nil - @config[:ping_timeout] = @config[:ping_timeout].present? ? @config[:ping_timeout].to_i : DEFAULT_PING_TIMEOUT + # 0 disables checkout ping; nil uses DEFAULT_PING_TIMEOUT. Avoid .present? + # so an explicit 0 is not treated as missing. + @config[:ping_timeout] = if @config.key?(:ping_timeout) && !@config[:ping_timeout].nil? + @config[:ping_timeout].to_i + else + DEFAULT_PING_TIMEOUT + end @config[:encoding] = @config[:encoding].present? ? @config[:encoding] : nil @connection_parameters ||= @config @@ -293,7 +299,7 @@ def disable_referential_integrity def active? return false unless @raw_connection - if raw_connection_pingable? + if raw_connection_ping_enabled? return false unless @raw_connection.ping(timeout: ping_timeout_seconds) verified! @@ -531,13 +537,14 @@ def connection_errors end end - def raw_connection_pingable? - @raw_connection.respond_to?(:ping) + def raw_connection_ping_enabled? + @raw_connection.respond_to?(:ping) && ping_timeout_seconds.positive? end def ping_timeout_seconds seconds = @config[:ping_timeout] - seconds = DEFAULT_PING_TIMEOUT if seconds.nil? || seconds.to_i <= 0 + return DEFAULT_PING_TIMEOUT if seconds.nil? + seconds.to_i end From c58f47737f06086a855e6e24771704d3ed743e21 Mon Sep 17 00:00:00 2001 From: John Rhoads Date: Wed, 16 Sep 2026 12:14:12 -0600 Subject: [PATCH 3/4] Clarify CHANGELOG: reference #1121 and spell out TCP RST Co-authored-by: Cursor --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a27f8fa3..3f9165f22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ #### Added - [#1385](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1385) Added support for Nulls first and last. -- Use TinyTDS `Client#ping(timeout:)` from `active?` when available so idle RST / half-dead checkouts are detected before the next statement (see [tiny_tds#609](https://github.com/rails-sqlserver/tiny_tds/pull/609), [#1396](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/1396)). Falls back to passive `active?` on older TinyTDS. Configure with `:ping_timeout` (seconds, default 2; `0` disables ping). +- Use TinyTDS `Client#ping(timeout:)` from `active?` when available so idle TCP RST (reset) / half-dead checkouts are detected before the next statement (see [tiny_tds#609](https://github.com/rails-sqlserver/tiny_tds/pull/609), [#1396](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/1396)). Restores checkout probing in the spirit of pre-[#1121](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1121) `SELECT 1` and of Postgres/Trilogy. Falls back to passive `active?` on older TinyTDS. Configure with `:ping_timeout` (seconds, default 2; `0` disables ping). #### Changed From f47d460a3f507d3b42c01cb509d4e23fe2bdc9b0 Mon Sep 17 00:00:00 2001 From: John Rhoads Date: Wed, 16 Sep 2026 12:30:06 -0600 Subject: [PATCH 4/4] Move changelog entry to Changed to match project convention Co-authored-by: Cursor --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9165f22..313130cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ #### Added - [#1385](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1385) Added support for Nulls first and last. -- Use TinyTDS `Client#ping(timeout:)` from `active?` when available so idle TCP RST (reset) / half-dead checkouts are detected before the next statement (see [tiny_tds#609](https://github.com/rails-sqlserver/tiny_tds/pull/609), [#1396](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/1396)). Restores checkout probing in the spirit of pre-[#1121](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1121) `SELECT 1` and of Postgres/Trilogy. Falls back to passive `active?` on older TinyTDS. Configure with `:ping_timeout` (seconds, default 2; `0` disables ping). #### Changed @@ -12,5 +11,6 @@ - [#1397](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1397) Treat "DBPROCESS is dead or not enabled" as ConnectionNotEstablished. - [#1406](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1406) Stop deserializing column defaults. - [#1405](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1405) Fix `insert_all`/`upsert_all` for single character, temporary, non-ASCII and three part table names, and stop an aliased target being included in the `MERGE` table name. +- [#1412](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1412) Use TinyTDS `Client#ping(timeout:)` from `active?` when available. Please check [8-1-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/8-1-stable/CHANGELOG.md) for previous changes.