Skip to content

Commit 0dc8e10

Browse files
committed
Add auto reconnect support utilizing a new #with_auto_reconnect block. By default each query run through the adapter will automatically reconnect at standard intervals, logging attempts along the way, till success or the original exception bubbles up. See docs for more details. Resolves ticket #18 http://rails-sqlserver.lighthouseapp.com/projects/20277/tickets/18-sql-server-adapter-doesnt-reconnect-on-lost-connection
1 parent 4986e2f commit 0dc8e10

File tree

4 files changed

+124
-14
lines changed

4 files changed

+124
-14
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11

22
MASTER
33

4+
* Add auto reconnect support utilizing a new #with_auto_reconnect block. By default each query run through
5+
the adapter will automatically reconnect at standard intervals, logging attempts along the way, till success
6+
or the original exception bubbles up. See docs for more details. Resolves ticket #18 [Ken Collins]
7+
48
* Update internal helper method #orders_and_dirs_set to cope with an order clause like "description desc". This
59
resolves ticket #26 [Ken Collins]
610

README.rdoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The SQL Server adapter for rails is back for ActiveRecord 2.2 and up! We are cur
1616
* Enabled #case_sensitive_equality_operator used by unique validations.
1717
* Unicode character support for nchar, nvarchar and ntext data types. Configuration option for defaulting all string data types to the unicode safe types.
1818
* View support for table names, identity inserts, and column defaults.
19+
* A block method to run queries within a specific isolation level.
20+
* Automatically reconnects to lost database connections.
1921

2022
==== Date/Time Data Type Hinting
2123

@@ -103,6 +105,12 @@ By default all queries to the INFORMATION_SCHEMA table is silenced. If you think
103105

104106
ActiveRecord::ConnectionAdapters::SQLServerAdapter.log_info_schema_queries = true
105107

108+
==== Auto Connecting
109+
110+
By default the adapter will auto connect to lost DB connections. For every query it will retry at intervals of 2, 4, 8, 16 and 32 seconds. During each retry it will callback out to ActiveRecord::Base.did_retry_sqlserver_connection(connection,count). When all retries fail, it will callback to ActiveRecord::Base.did_loose_sqlserver_connection(connection). Both implementations of these methods are to write to the rails logger, however, they make great override points for notifications like Hoptoad. If you want to disable automatic reconnections use the following in an initializer.
111+
112+
ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = false
113+
106114

107115
== Versions
108116

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@ def self.sqlserver_connection(config) #:nodoc:
2323
host = config[:host] ? config[:host].to_s : 'localhost'
2424
driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
2525
end
26-
conn = DBI.connect(driver_url, username, password)
27-
conn["AutoCommit"] = true
28-
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
26+
ConnectionAdapters::SQLServerAdapter.new(logger, [driver_url, username, password])
27+
end
28+
29+
protected
30+
31+
def self.did_retry_sqlserver_connection(connection,count)
32+
logger.info "CONNECTION RETRY: #{connection.class.name} retry ##{count}."
33+
end
34+
35+
def self.did_loose_sqlserver_connection(connection)
36+
logger.info "CONNECTION LOST: #{connection.class.name}"
2937
end
3038

3139
end
@@ -155,8 +163,15 @@ class SQLServerAdapter < AbstractAdapter
155163
SUPPORTED_VERSIONS = [2000,2005].freeze
156164
LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
157165

166+
LOST_CONNECTION_EXCEPTIONS = [DBI::DatabaseError, DBI::InterfaceError]
167+
LOST_CONNECTION_MESSAGES = [
168+
'Communication link failure',
169+
'Read from the server failed',
170+
'Write to the server failed',
171+
'Database connection was already closed']
172+
158173
cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
159-
:log_info_schema_queries, :enable_default_unicode_types
174+
:log_info_schema_queries, :enable_default_unicode_types, :auto_connect
160175

161176
class << self
162177

@@ -166,9 +181,10 @@ def type_limitable?(type)
166181

167182
end
168183

169-
def initialize(connection, logger, connection_options=nil)
170-
super(connection, logger)
184+
def initialize(logger, connection_options)
171185
@connection_options = connection_options
186+
connect
187+
super(raw_connection, logger)
172188
initialize_sqlserver_caches
173189
unless SUPPORTED_VERSIONS.include?(database_year)
174190
raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
@@ -221,6 +237,10 @@ def inspect
221237
"#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
222238
end
223239

240+
def auto_connect
241+
@@auto_connect.is_a?(FalseClass) ? false : true
242+
end
243+
224244
def native_string_database_type
225245
@@native_string_database_type || (enable_default_unicode_types ? 'nvarchar' : 'varchar')
226246
end
@@ -302,16 +322,14 @@ def disable_referential_integrity(&block)
302322
def active?
303323
raw_connection.execute("SELECT 1").finish
304324
true
305-
rescue DBI::DatabaseError, DBI::InterfaceError
325+
rescue *LOST_CONNECTION_EXCEPTIONS
306326
false
307327
end
308328

309329
def reconnect!
310330
disconnect!
311-
@connection = DBI.connect(*@connection_options)
312-
rescue DBI::DatabaseError => e
313-
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
314-
false
331+
connect
332+
active?
315333
end
316334

317335
def disconnect!
@@ -721,6 +739,47 @@ def remove_database_connections_and_rollback(name)
721739

722740
protected
723741

742+
# CONNECTION MANAGEMENT ====================================#
743+
744+
def connect
745+
driver_url, username, password = @connection_options
746+
@connection = DBI.connect(driver_url, username, password)
747+
configure_connection
748+
rescue
749+
raise unless @auto_connecting
750+
end
751+
752+
def configure_connection
753+
raw_connection['AutoCommit'] = true
754+
end
755+
756+
def with_auto_reconnect
757+
begin
758+
yield
759+
rescue *LOST_CONNECTION_EXCEPTIONS => e
760+
if LOST_CONNECTION_MESSAGES.any? { |lcm| e.message =~ Regexp.new(lcm,Regexp::IGNORECASE) }
761+
retry if auto_reconnected?
762+
end
763+
raise
764+
end
765+
end
766+
767+
def auto_reconnected?
768+
return false unless auto_connect
769+
@auto_connecting = true
770+
count = 0
771+
while count <= 5
772+
sleep 2** count
773+
ActiveRecord::Base.did_retry_sqlserver_connection(self,count)
774+
return true if connect && active?
775+
count += 1
776+
end
777+
ActiveRecord::Base.did_loose_sqlserver_connection(self)
778+
false
779+
ensure
780+
@auto_connecting = false
781+
end
782+
724783
# DATABASE STATEMENTS ======================================
725784

726785
def select(sql, name = nil, ignore_special_columns = false)
@@ -751,9 +810,9 @@ def info_schema_query
751810
def raw_execute(sql, name = nil, &block)
752811
log(sql, name) do
753812
if block_given?
754-
raw_connection.execute(sql) { |handle| yield(handle) }
813+
with_auto_reconnect { raw_connection.execute(sql) { |handle| yield(handle) } }
755814
else
756-
raw_connection.execute(sql)
815+
with_auto_reconnect { raw_connection.execute(sql) }
757816
end
758817
end
759818
end
@@ -767,7 +826,7 @@ def without_type_conversion
767826

768827
def do_execute(sql,name=nil)
769828
log(sql, name || 'EXECUTE') do
770-
raw_connection.do(sql)
829+
with_auto_reconnect { raw_connection.do(sql) }
771830
end
772831
end
773832

test/cases/connection_test_sqlserver.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,37 @@ def setup
7878
end
7979
end
8080

81+
context 'Connection management' do
82+
83+
setup do
84+
assert @connection.active?
85+
end
86+
87+
should 'be able to disconnect and reconnect at will' do
88+
@connection.disconnect!
89+
assert !@connection.active?
90+
@connection.reconnect!
91+
assert @connection.active?
92+
end
93+
94+
should 'auto reconnect when setting is on' do
95+
with_auto_connect(true) do
96+
@connection.disconnect!
97+
assert_nothing_raised() { Topic.count }
98+
assert @connection.active?
99+
end
100+
end
101+
102+
should 'not auto reconnect when setting is off' do
103+
with_auto_connect(false) do
104+
@connection.disconnect!
105+
assert_raise(ActiveRecord::StatementInvalid) { Topic.count }
106+
end
107+
end
108+
109+
end
110+
111+
81112

82113
private
83114

@@ -99,5 +130,13 @@ def assert_all_statements_used_are_closed(&block)
99130
ensure
100131
GC.enable
101132
end
133+
134+
def with_auto_connect(boolean)
135+
existing = ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect
136+
ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = boolean
137+
yield
138+
ensure
139+
ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = existing
140+
end
102141

103142
end

0 commit comments

Comments
 (0)