Created
May 4, 2010 17:36
-
-
Save jrafanie/389706 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 54799c96a46237c101cc9290d89a86610a17d14c Mon Sep 17 00:00:00 2001 | |
From: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue, 4 May 2010 13:13:27 -0400 | |
Subject: [PATCH] Squashed commit of the following: | |
commit a5f3f44c88e848f6862632e3f3587efb9bc9e4b9 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue May 4 13:03:47 2010 -0400 | |
-Fixed transaction location to public methods. | |
-Use @logger since logger method is not defined | |
commit f9611b15fc57054eb0c7e80d016edba4aef002dc | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue May 4 11:56:11 2010 -0400 | |
Squashed commit of the following: | |
commit 3a4fd24e447f4bf5916a9d77444e8513993c0b50 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue May 4 11:09:44 2010 -0400 | |
Strip any company stuff | |
commit cd9d166353b4cb7ac7885ce41664e9dbebb987f6 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Wed Apr 21 11:57:59 2010 -0400 | |
Cleanup after merging upstream changes (support for returning multiple | |
result sets, issues #29): | |
Initialize the local variable outside the block, because it is only | |
set within a block and later is returned outside the block. Move begin/ensure | |
handle cleanup logic into the loop. | |
commit 9536776df4e9e2e02764474ad655356027b190af | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Wed Apr 14 18:11:23 2010 -0400 | |
More 1205 deadlock victim cleanup: | |
Create error_is_a_1205? method to be more DRY. | |
Bring over log method so it doesn't change our exception in the case of 1205 and miss our retry logic. | |
In transaction, SQL server will automatically rollback on a 1205 "deadlock victim" error so don't initiate a rollback and instead retry the transaction. | |
commit da0c75fb3eaea478646508e90ab95de4ba1bd9d2 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Wed Apr 14 18:07:35 2010 -0400 | |
Move ensure finish statement handle from handle_as_array to raw_select since that's where it's created | |
commit 978ba0d6b2832bfc0b34caba8cbda1f2c9016050 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Wed Apr 14 18:05:42 2010 -0400 | |
Don't reconnect when in the middle of a transaction | |
commit 6c03b45a016246e99ea75feb21ce592f36e05835 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Wed Apr 14 18:04:54 2010 -0400 | |
Ternary operator is great but you don't know what method call blew up when looking at the line number in the callstack. | |
commit 637512df7a2f6253ec71c58d9edb844ed2eb3537 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Wed Apr 14 18:03:55 2010 -0400 | |
Ensure we finish the statement handle in spid method | |
commit e2712eccbf771956a483d6ce7a420add76f65ae0 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue Apr 13 11:01:48 2010 -0400 | |
Log messages as warnings since that's what they are. | |
commit 60995a0d03beeba7fdb3d94ba909e4d64202274f | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue Apr 13 11:01:12 2010 -0400 | |
Add methods to retrieve database version, product version, product level, and edition: | |
Ie: product version: 10.0.2531.0, product level: SP1, edition: Enterprise Edition (64-bit) | |
commit 5531436c380a299654db4de480366d3a05d9a018 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue Apr 13 14:54:22 2010 -0400 | |
Do not use do_execute and it's auto-reconnect logic for commit_db_transaction/rollback_db_transaction since we could get a new connection without the transaction context. | |
commit fde84f1597e62800e9574e8c80487a92cbe24e4e | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue Apr 13 13:45:10 2010 -0400 | |
Retry ODBC::Error 1205 Deadlock victim errors: (http://stackoverflow.com/questions/1980937/list-of-sql-server-errors-that-should-be-retried) | |
If in a transaction, retry in the overloaded transaction method, otherwise, retry in the raw_connection_run/raw_connection_do. | |
Log up to 4k of the sql statement. | |
In raw_select, retry the whole method since this error could occur when requesting the handle or whenever we fetch rows on this handle. | |
Wrap the whole raw_select in a log block to get full timings to get the statement handle, fetch each row, and store all the rows in memory. | |
In handle_as_array, ensure that we finish the statement handle since we could get an error such as the 1205 in the middle of the method and leak the handle. | |
commit 48d2a55833630b05adef345d936e37992d4a4a35 | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue Apr 13 13:41:59 2010 -0400 | |
Tests for connection isolation level, lock timeout, and spid | |
commit 12faea458050c04fe4f9c112196188b20da163fb | |
Author: Joe Rafaniello <jrafanie@gmail.com> | |
Date: Tue Apr 13 11:18:17 2010 -0400 | |
Add connection options which set class variables for transaction isolation level and lock timeout. | |
Add spid method so we can log server pids. | |
Apply lock timeout, trans isolation level, and log the spid during initialize and in reconnect! | |
--- | |
.../connection_adapters/sqlserver_adapter.rb | 206 ++++++++++++++++++-- | |
test/cases/connection_test_sqlserver.rb | 71 +++++++- | |
2 files changed, 253 insertions(+), 24 deletions(-) | |
diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb | |
index 0968b7d..839782f 100644 | |
--- a/lib/active_record/connection_adapters/sqlserver_adapter.rb | |
+++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb | |
@@ -31,11 +31,11 @@ module ActiveRecord | |
protected | |
def self.did_retry_sqlserver_connection(connection,count) | |
- logger.info "CONNECTION RETRY: #{connection.class.name} retry ##{count}." | |
+ @logger.warn "CONNECTION RETRY: #{connection.class.name} retry ##{count}." if @logger | |
end | |
def self.did_lose_sqlserver_connection(connection) | |
- logger.info "CONNECTION LOST: #{connection.class.name}" | |
+ @logger.warn "CONNECTION LOST: #{connection.class.name}" if @logger | |
end | |
end | |
@@ -175,9 +175,15 @@ module ActiveRecord | |
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i], | |
:adonet => [/current state is closed/, /network-related/] | |
} | |
+ | |
+ # Timeout error is raised differently depending on the patch level of | |
+ # ruby 1.8.6. Patch 111 raises Timeout::Error and 287 raises | |
+ # Timeout::ExitException. | |
+ TIMEOUT_ERROR = Timeout.const_defined?(:ExitException) ? Timeout::ExitException : Timeout::Error | |
+ | |
cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type, | |
- :log_info_schema_queries, :enable_default_unicode_types, :auto_connect | |
+ :log_info_schema_queries, :enable_default_unicode_types, :auto_connect, :transaction_isolation_level, :lock_timeout | |
class << self | |
@@ -189,16 +195,34 @@ module ActiveRecord | |
def initialize(logger,config) | |
@connection_options = config | |
+ @sql_retry_message_length = 4096 | |
connect | |
super(raw_connection, logger) | |
initialize_sqlserver_caches | |
unless SUPPORTED_VERSIONS.include?(database_year) | |
raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported." | |
end | |
+ | |
+ apply_connection_config | |
+ @logger.info "(#{adapter_name}.initialize) SPID: [#{spid}]" if @logger | |
end | |
# ABSTRACT ADAPTER =========================================# | |
- | |
+ | |
+ def apply_connection_config | |
+ @connection_options[:trans_isolation_level] ||= @@transaction_isolation_level | |
+ @connection_options[:lock_timeout] ||= @@lock_timeout | |
+ @logger.info "(#{adapter_name}.apply_connection_config) trans_isolation_level: [#{@connection_options[:trans_isolation_level]}], lock_timeout: [#{@connection_options[:lock_timeout]}]" if @logger | |
+ | |
+ # IMPORTANT: Perform all executes with the raw_connection as we could already be in a auto reconnect log block, ie, don't use do_execute | |
+ begin | |
+ raw_connection_do("SET TRANSACTION ISOLATION LEVEL #{@connection_options[:trans_isolation_level]}") unless @connection_options[:trans_isolation_level].nil? | |
+ raw_connection_do("SET LOCK_TIMEOUT #{@connection_options[:lock_timeout]}") unless @connection_options[:lock_timeout].nil? | |
+ rescue | |
+ raise unless @auto_connecting | |
+ end | |
+ end | |
+ | |
def adapter_name | |
ADAPTER_NAME | |
end | |
@@ -242,11 +266,39 @@ module ActiveRecord | |
def version | |
self.class::VERSION | |
end | |
- | |
+ | |
+ def detailed_database_version | |
+ "product version: #{product_version}, product level: #{product_level}, edition: #{edition}" | |
+ end | |
+ | |
+ def product_version | |
+ @product_version ||= info_schema_query { select_value("SELECT CAST(SERVERPROPERTY('productversion') AS VARCHAR(128))")} | |
+ end | |
+ | |
+ def product_level | |
+ @product_level ||= info_schema_query { select_value("SELECT CAST(SERVERPROPERTY('productlevel') AS VARCHAR(128))")} | |
+ end | |
+ | |
+ def edition | |
+ @edition ||= info_schema_query { select_value("SELECT CAST(SERVERPROPERTY('edition') AS VARCHAR(128))")} | |
+ end | |
+ | |
+ def spid | |
+ # Since spid method is called in the reconnect logic, we need to run the query outside of the log and auto reconnect block via raw_connection.run | |
+ @spid = raw_connection.run("SELECT @@spid") do |sth| | |
+ begin | |
+ res = sth.fetch | |
+ res.respond_to?(:first) ? res.first : res | |
+ ensure | |
+ finish_statement_handle(sth) | |
+ end | |
+ end | |
+ end | |
+ | |
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 | |
@@ -339,6 +391,8 @@ module ActiveRecord | |
def reconnect! | |
disconnect! | |
connect | |
+ apply_connection_config | |
+ @logger.info "(#{adapter_name}.reconnect!) SPID: [#{spid}]" if @logger | |
active? | |
end | |
@@ -410,11 +464,13 @@ module ActiveRecord | |
end | |
def commit_db_transaction | |
- do_execute "COMMIT TRANSACTION" | |
+ sql = "COMMIT TRANSACTION" | |
+ log(sql, 'EXECUTE') { raw_connection_do(sql) } | |
end | |
def rollback_db_transaction | |
- do_execute "ROLLBACK TRANSACTION" rescue nil | |
+ sql = "ROLLBACK TRANSACTION" | |
+ log(sql, 'EXECUTE') { raw_connection_do(sql) } rescue nil | |
end | |
def create_savepoint | |
@@ -427,6 +483,43 @@ module ActiveRecord | |
def rollback_to_savepoint | |
do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}" | |
end | |
+ | |
+ # Wrap a block in a transaction. Returns result of block. | |
+ def transaction(start_db_transaction = true) | |
+ transaction_open = false | |
+ begin | |
+ if block_given? | |
+ if start_db_transaction | |
+ begin_db_transaction | |
+ transaction_open = true | |
+ end | |
+ yield | |
+ end | |
+ rescue Exception => err | |
+ if transaction_open | |
+ transaction_open = false | |
+ if error_is_a_1205?(err) | |
+ # TODO: Parse error to extract the sql statement so it can be logged. | |
+ # SQL Server rolls back transaction when raising 1205 error | |
+ @logger.warn("(#{adapter_name}.transaction) SPID: [#{@spid}] Retrying due to 1205 response from sql server...") if @logger | |
+ retry | |
+ else | |
+ rollback_db_transaction | |
+ end | |
+ end | |
+ | |
+ raise unless err.is_a? ActiveRecord::Rollback | |
+ end | |
+ ensure | |
+ if transaction_open | |
+ begin | |
+ commit_db_transaction | |
+ rescue Exception => err | |
+ rollback_db_transaction | |
+ raise | |
+ end | |
+ end | |
+ end | |
def add_limit_offset!(sql, options) | |
# Validate and/or convert integers for :limit and :offets options. | |
@@ -807,6 +900,7 @@ module ActiveRecord | |
def auto_reconnected? | |
return false unless auto_connect | |
+ return false if self.open_transactions > 0 # don't switch connection if in the middle of a transaction | |
@auto_connecting = true | |
count = 0 | |
while count <= 5 | |
@@ -825,7 +919,19 @@ module ActiveRecord | |
with_auto_reconnect do | |
case connection_mode | |
when :odbc | |
- block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql) | |
+ begin | |
+ if block_given? | |
+ raw_connection.run_block(sql) { |handle| yield(handle) } | |
+ else | |
+ raw_connection.run(sql) | |
+ end | |
+ rescue ODBC::Error => e # Retry ODBC 1205 "deadlock victim" errors | |
+ if error_is_a_1205?(e) && self.open_transactions == 0 | |
+ @logger.warn("(#{adapter_name}.raw_connection_run) SPID: [#{@spid}] Retrying due to 1205 response from sql server... SQL: [#{sql.to_s[0..@sql_retry_message_length]}]") if @logger | |
+ retry | |
+ end | |
+ raise | |
+ end | |
else :adonet | |
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_reader | |
end | |
@@ -835,7 +941,15 @@ module ActiveRecord | |
def raw_connection_do(sql) | |
case connection_mode | |
when :odbc | |
- raw_connection.do(sql) | |
+ begin | |
+ raw_connection.do(sql) | |
+ rescue ODBC::Error => e # Retry ODBC 1205 "deadlock victim" errors | |
+ if error_is_a_1205?(e) && self.open_transactions == 0 | |
+ @logger.warn("(#{adapter_name}.raw_connection_do) SPID: [#{@spid}] Retrying due to 1205 response from sql server... SQL: [#{sql.to_s[0..@sql_retry_message_length]}]") if @logger | |
+ retry | |
+ end | |
+ raise | |
+ end | |
else :adonet | |
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_non_query | |
end | |
@@ -897,19 +1011,28 @@ module ActiveRecord | |
fields_and_row_sets = [] | |
log(sql,name) do | |
begin | |
+ handle = nil | |
handle = raw_connection_run(sql) | |
- loop do | |
- fields_and_rows = case connection_mode | |
- when :odbc | |
- handle_to_fields_and_rows_odbc(handle) | |
- when :adonet | |
- handle_to_fields_and_rows_adonet(handle) | |
- end | |
- fields_and_row_sets << fields_and_rows | |
- break unless handle_more_results?(handle) | |
+ begin | |
+ loop do | |
+ fields_and_rows = case connection_mode | |
+ when :odbc | |
+ handle_to_fields_and_rows_odbc(handle) | |
+ when :adonet | |
+ handle_to_fields_and_rows_adonet(handle) | |
+ end | |
+ fields_and_row_sets << fields_and_rows | |
+ break unless handle_more_results?(handle) | |
+ end | |
+ ensure | |
+ finish_statement_handle(handle) | |
end | |
- ensure | |
- finish_statement_handle(handle) | |
+ rescue ODBC::Error => e # Retry ODBC 1205 "deadlock victim" errors | |
+ if error_is_a_1205?(e) | |
+ @logger.warn("(#{adapter_name}.raw_select) SPID: [#{@spid}] Retrying due to 1205 response from sql server... SQL: [#{sql.to_s[0..@sql_retry_message_length]}]") if @logger | |
+ retry | |
+ end | |
+ raise | |
end | |
end | |
fields_and_row_sets | |
@@ -1050,6 +1173,47 @@ module ActiveRecord | |
# HELPER METHODS ===========================================# | |
+ def log(sql, name) | |
+ if block_given? | |
+ result = nil | |
+ seconds = Benchmark.realtime { result = yield } | |
+ @runtime += seconds | |
+ log_info(sql, name, seconds) | |
+ result | |
+ else | |
+ log_info(sql, name, 0) | |
+ nil | |
+ end | |
+ rescue TIMEOUT_ERROR => e | |
+ # Rescue timeout error seperately to avoid getting it raised as ActiveRecord::StatementInvalid | |
+ message = "Super class: [#{e.class.superclass.name}], Class: [#{e.class.name}], Message: [#{e.message}], Sql: [#{sql[0..1000]}...]" # need to reference superclass because the timeout implementation creates a subclass | |
+ @logger.error("(abstract_adapter) Name: [#{name}], Message: [#{message}]") if @logger | |
+ @logger.info("(abstract_adapter) Verifying DB connection after timeout error") if @logger | |
+ begin | |
+ verify! | |
+ rescue => err | |
+ @logger.info("(abstract_adapter) Reconnecting after timeout error failed due to err: Class: [#{err.class.name}], Message: [#{err.message}]") if @logger | |
+ end | |
+ raise e | |
+ rescue Exception => e | |
+ raise if error_is_a_1205?(e) | |
+ | |
+ # Log message and raise exception. | |
+ # Set last_verification to 0, so that connection gets verified | |
+ # upon reentering the request loop | |
+ @last_verification = 0 | |
+ message = "#{e.class.name}: #{e.message}: #{sql[0..1000]}..." | |
+ log_info(message, name, 0) | |
+ | |
+ @logger.error("(abstract_adapter) Name: [#{name}], Message: [#{message}]") if @logger | |
+ | |
+ raise ActiveRecord::StatementInvalid, message | |
+ end | |
+ | |
+ def error_is_a_1205?(err) | |
+ err.kind_of?(ODBC::Error) && err.message.include?("(1205)") | |
+ end | |
+ | |
def insert_sql?(sql) | |
!(sql =~ /^\s*INSERT/i).nil? | |
end | |
diff --git a/test/cases/connection_test_sqlserver.rb b/test/cases/connection_test_sqlserver.rb | |
index f2dafbe..0453ec0 100644 | |
--- a/test/cases/connection_test_sqlserver.rb | |
+++ b/test/cases/connection_test_sqlserver.rb | |
@@ -98,10 +98,75 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase | |
assert_raise(ActiveRecord::StatementInvalid) { Topic.count } | |
end | |
end | |
- | |
+ | |
+ context "spid method" do | |
+ setup do | |
+ @spid = @connection.spid | |
+ end | |
+ | |
+ should "return a fixnum" do | |
+ assert @spid.is_a?(Fixnum) | |
+ end | |
+ | |
+ should "set instance variable" do | |
+ assert_equal @spid, @connection.instance_variable_get(:@spid) | |
+ end | |
+ end | |
+ | |
+ context "connection transaction isolation level" do | |
+ setup do | |
+ @iso_level = "READ UNCOMMITTED" | |
+ @connection.transaction_isolation_level = @iso_level | |
+ end | |
+ | |
+ teardown do | |
+ @connection.transaction_isolation_level = nil | |
+ opts = @connection.instance_variable_get(:@connection_options).merge({:trans_isolation_level => nil}) | |
+ @connection.instance_variable_set(:@connection_options, opts) | |
+ end | |
+ | |
+ should "set Adapter class variable" do | |
+ assert_equal @iso_level, ActiveRecord::ConnectionAdapters::SQLServerAdapter.transaction_isolation_level | |
+ end | |
+ | |
+ should 'apply isolation level via apply_connection_config' do | |
+ assert_nothing_raised() {@connection.apply_connection_config} | |
+ end | |
+ | |
+ should 'apply isolation level on reconnect!' do | |
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.any_instance.expects(:apply_connection_config) | |
+ assert_nothing_raised() {@connection.reconnect!} | |
+ end | |
+ end | |
+ | |
+ context "connection lock timeout" do | |
+ setup do | |
+ @lock_timeout = 30000 | |
+ @connection.lock_timeout = @lock_timeout | |
+ end | |
+ | |
+ teardown do | |
+ @connection.lock_timeout = nil | |
+ opts = @connection.instance_variable_get(:@connection_options).merge({:lock_timeout => nil}) | |
+ @connection.instance_variable_set(:@connection_options, opts) | |
+ end | |
+ | |
+ should "set Adapter class variable" do | |
+ assert_equal @lock_timeout, ActiveRecord::ConnectionAdapters::SQLServerAdapter.lock_timeout | |
+ end | |
+ | |
+ should 'apply lock timeout via apply_connection_config' do | |
+ assert_nothing_raised() {@connection.apply_connection_config} | |
+ end | |
+ | |
+ should 'apply lock timeout on reconnect!' do | |
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.any_instance.expects(:apply_connection_config) | |
+ assert_nothing_raised() {@connection.reconnect!} | |
+ end | |
+ end | |
end | |
- | |
- | |
+ | |
+ | |
private | |
-- | |
1.6.5 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment