Skip to content

Commit

Permalink
Add auto reconnect support utilizing a new #with_auto_reconnect block…
Browse files Browse the repository at this point in the history
…. 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 rails-sqlserver#18 http://rails-sqlserver.lighthouseapp.com/projects/20277/tickets/18-sql-server-adapter-doesnt-reconnect-on-lost-connection
  • Loading branch information
metaskills committed Apr 21, 2009
1 parent 4986e2f commit 0dc8e10
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
@@ -1,6 +1,10 @@

MASTER

* 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 [Ken Collins]

* Update internal helper method #orders_and_dirs_set to cope with an order clause like "description desc". This
resolves ticket #26 [Ken Collins]

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

==== Date/Time Data Type Hinting

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

ActiveRecord::ConnectionAdapters::SQLServerAdapter.log_info_schema_queries = true

==== Auto Connecting

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.

ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = false


== Versions

Expand Down
87 changes: 73 additions & 14 deletions lib/active_record/connection_adapters/sqlserver_adapter.rb
Expand Up @@ -23,9 +23,17 @@ def self.sqlserver_connection(config) #:nodoc:
host = config[:host] ? config[:host].to_s : 'localhost'
driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
end
conn = DBI.connect(driver_url, username, password)
conn["AutoCommit"] = true
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
ConnectionAdapters::SQLServerAdapter.new(logger, [driver_url, username, password])
end

protected

def self.did_retry_sqlserver_connection(connection,count)
logger.info "CONNECTION RETRY: #{connection.class.name} retry ##{count}."
end

def self.did_loose_sqlserver_connection(connection)
logger.info "CONNECTION LOST: #{connection.class.name}"
end

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

LOST_CONNECTION_EXCEPTIONS = [DBI::DatabaseError, DBI::InterfaceError]
LOST_CONNECTION_MESSAGES = [
'Communication link failure',
'Read from the server failed',
'Write to the server failed',
'Database connection was already closed']

cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
:log_info_schema_queries, :enable_default_unicode_types
:log_info_schema_queries, :enable_default_unicode_types, :auto_connect

class << self

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

end

def initialize(connection, logger, connection_options=nil)
super(connection, logger)
def initialize(logger, connection_options)
@connection_options = connection_options
connect
super(raw_connection, logger)
initialize_sqlserver_caches
unless SUPPORTED_VERSIONS.include?(database_year)
raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
Expand Down Expand Up @@ -221,6 +237,10 @@ def inspect
"#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
end

def auto_connect
@@auto_connect.is_a?(FalseClass) ? false : true
end

def native_string_database_type
@@native_string_database_type || (enable_default_unicode_types ? 'nvarchar' : 'varchar')
end
Expand Down Expand Up @@ -302,16 +322,14 @@ def disable_referential_integrity(&block)
def active?
raw_connection.execute("SELECT 1").finish
true
rescue DBI::DatabaseError, DBI::InterfaceError
rescue *LOST_CONNECTION_EXCEPTIONS
false
end

def reconnect!
disconnect!
@connection = DBI.connect(*@connection_options)
rescue DBI::DatabaseError => e
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
false
connect
active?
end

def disconnect!
Expand Down Expand Up @@ -721,6 +739,47 @@ def remove_database_connections_and_rollback(name)

protected

# CONNECTION MANAGEMENT ====================================#

def connect
driver_url, username, password = @connection_options
@connection = DBI.connect(driver_url, username, password)
configure_connection
rescue
raise unless @auto_connecting
end

def configure_connection
raw_connection['AutoCommit'] = true
end

def with_auto_reconnect
begin
yield
rescue *LOST_CONNECTION_EXCEPTIONS => e
if LOST_CONNECTION_MESSAGES.any? { |lcm| e.message =~ Regexp.new(lcm,Regexp::IGNORECASE) }
retry if auto_reconnected?
end
raise
end
end

def auto_reconnected?
return false unless auto_connect
@auto_connecting = true
count = 0
while count <= 5
sleep 2** count
ActiveRecord::Base.did_retry_sqlserver_connection(self,count)
return true if connect && active?
count += 1
end
ActiveRecord::Base.did_loose_sqlserver_connection(self)
false
ensure
@auto_connecting = false
end

# DATABASE STATEMENTS ======================================

def select(sql, name = nil, ignore_special_columns = false)
Expand Down Expand Up @@ -751,9 +810,9 @@ def info_schema_query
def raw_execute(sql, name = nil, &block)
log(sql, name) do
if block_given?
raw_connection.execute(sql) { |handle| yield(handle) }
with_auto_reconnect { raw_connection.execute(sql) { |handle| yield(handle) } }
else
raw_connection.execute(sql)
with_auto_reconnect { raw_connection.execute(sql) }
end
end
end
Expand All @@ -767,7 +826,7 @@ def without_type_conversion

def do_execute(sql,name=nil)
log(sql, name || 'EXECUTE') do
raw_connection.do(sql)
with_auto_reconnect { raw_connection.do(sql) }
end
end

Expand Down
39 changes: 39 additions & 0 deletions test/cases/connection_test_sqlserver.rb
Expand Up @@ -78,6 +78,37 @@ def setup
end
end

context 'Connection management' do

setup do
assert @connection.active?
end

should 'be able to disconnect and reconnect at will' do
@connection.disconnect!
assert !@connection.active?
@connection.reconnect!
assert @connection.active?
end

should 'auto reconnect when setting is on' do
with_auto_connect(true) do
@connection.disconnect!
assert_nothing_raised() { Topic.count }
assert @connection.active?
end
end

should 'not auto reconnect when setting is off' do
with_auto_connect(false) do
@connection.disconnect!
assert_raise(ActiveRecord::StatementInvalid) { Topic.count }
end
end

end



private

Expand All @@ -99,5 +130,13 @@ def assert_all_statements_used_are_closed(&block)
ensure
GC.enable
end

def with_auto_connect(boolean)
existing = ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect
ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = boolean
yield
ensure
ActiveRecord::ConnectionAdapters::SQLServerAdapter.auto_connect = existing
end

end

0 comments on commit 0dc8e10

Please sign in to comment.