diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 1acfe11f73..fd5f8393b3 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,8 @@ +* Adapter `#execute` methods now accept an `allow_retry` option. When set to `true`, the SQL statement will be + retried, up to the database's configured `connection_retries` value, upon encountering connection-related errors. + + *Adrianna Chang* + * Only trigger `after_commit :destroy` callbacks when a database row is deleted. This prevents `after_commit :destroy` callbacks from being triggered again diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 662354eac7..103fb72bf7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -110,10 +110,15 @@ def write_query?(sql) # Executes the SQL statement in the context of this connection and returns # the raw result from the connection adapter. + # + # Setting +allow_retry+ to true causes the db to reconnect and retry + # executing the SQL statement in case of a connection-related exception. + # This option should only be enabled for known idempotent queries. + # # Note: depending on your database connector, the result returned by this # method may be manually memory managed. Consider using the exec_query # wrapper instead. - def execute(sql, name = nil) + def execute(sql, name = nil, allow_retry: false) raise NotImplementedError end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index cee461fd1e..94328d6e9d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -221,11 +221,15 @@ def disable_referential_integrity # :nodoc: #++ # Executes the SQL statement in the context of this connection. - def execute(sql, name = nil, async: false) + # + # Setting +allow_retry+ to true causes the db to reconnect and retry + # executing the SQL statement in case of a connection-related exception. + # This option should only be enabled for known idempotent queries. + def execute(sql, name = nil, async: false, allow_retry: false) sql = transform_query(sql) check_if_write_query(sql) - raw_execute(sql, name, async: async) + raw_execute(sql, name, async: async, allow_retry: allow_retry) end # Mysql2Adapter doesn't have to free a result after using it, but we use this method diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 4d41d46bb6..a9afa403e8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -33,15 +33,20 @@ def write_query?(sql) # :nodoc: # Executes an SQL statement, returning a PG::Result object on success # or raising a PG::Error exception otherwise. + # + # Setting +allow_retry+ to true causes the db to reconnect and retry + # executing the SQL statement in case of a connection-related exception. + # This option should only be enabled for known idempotent queries. + # # Note: the PG::Result object is manually memory managed; if you don't # need it specifically, you may want consider the exec_query wrapper. - def execute(sql, name = nil) + def execute(sql, name = nil, allow_retry: false) sql = transform_query(sql) check_if_write_query(sql) mark_transaction_written_if_write(sql) - with_raw_connection do |conn| + with_raw_connection(allow_retry: allow_retry) do |conn| log(sql, name) do conn.async_exec(sql) end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb index 2911d954c7..5e9602171c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb @@ -20,14 +20,14 @@ def explain(arel, binds = []) SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", [])) end - def execute(sql, name = nil) # :nodoc: + def execute(sql, name = nil, allow_retry: false) # :nodoc: sql = transform_query(sql) check_if_write_query(sql) mark_transaction_written_if_write(sql) log(sql, name) do - with_raw_connection do |conn| + with_raw_connection(allow_retry: allow_retry) do |conn| conn.execute(sql) end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 55ac076995..43501cfa36 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -690,6 +690,21 @@ def teardown end end + unless current_adapter?(:SQLite3Adapter) + test "#execute is retryable" do + conn_id = case @connection.class::ADAPTER_NAME + when "Mysql2" + @connection.execute("SELECT CONNECTION_ID()").to_a[0][0] + when "PostgreSQL" + @connection.execute("SELECT pg_backend_pid()").to_a[0]["pg_backend_pid"] + end + + kill_connection_from_server(conn_id) + + @connection.execute("SELECT 1", allow_retry: true) + end + end + private def raw_transaction_open?(connection) case connection.class::ADAPTER_NAME @@ -731,6 +746,18 @@ def remote_disconnect(connection) skip end end + + def kill_connection_from_server(connection_id) + conn = @connection.pool.checkout + case conn.class::ADAPTER_NAME + when "Mysql2" + conn.execute("KILL #{connection_id}") + when "PostgreSQL" + conn.execute("SELECT pg_cancel_backend(#{connection_id})") + end + + conn.close + end end end end