mirror of https://github.com/rails/rails
Use SQLite `IMMEDIATE` transactions when possible.
Transactions run against the SQLite3 adapter default to IMMEDIATE mode to improve concurrency support and avoid busy exceptions. Fixture transactions use DEFERRED mode transactions as all `joinable` transactions become DEFERRED transactions.
This commit is contained in:
parent
ac0fa17eae
commit
1e2c9048c4
|
@ -1,3 +1,9 @@
|
||||||
|
* Use SQLite `IMMEDIATE` transactions when possible.
|
||||||
|
|
||||||
|
Transactions run against the SQLite3 adapter default to IMMEDIATE mode to improve concurrency support and avoid busy exceptions.
|
||||||
|
|
||||||
|
*Stephen Margheim*
|
||||||
|
|
||||||
* Raise specific exception when a connection is not defined.
|
* Raise specific exception when a connection is not defined.
|
||||||
|
|
||||||
The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested.
|
The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested.
|
||||||
|
|
|
@ -411,6 +411,14 @@ module ActiveRecord
|
||||||
# Begins the transaction (and turns off auto-committing).
|
# Begins the transaction (and turns off auto-committing).
|
||||||
def begin_db_transaction() end
|
def begin_db_transaction() end
|
||||||
|
|
||||||
|
def begin_deferred_transaction(isolation_level = nil) # :nodoc:
|
||||||
|
if isolation_level
|
||||||
|
begin_isolated_db_transaction(isolation_level)
|
||||||
|
else
|
||||||
|
begin_db_transaction
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def transaction_isolation_levels
|
def transaction_isolation_levels
|
||||||
{
|
{
|
||||||
read_uncommitted: "READ UNCOMMITTED",
|
read_uncommitted: "READ UNCOMMITTED",
|
||||||
|
|
|
@ -448,10 +448,14 @@ module ActiveRecord
|
||||||
# = Active Record Real \Transaction
|
# = Active Record Real \Transaction
|
||||||
class RealTransaction < Transaction
|
class RealTransaction < Transaction
|
||||||
def materialize!
|
def materialize!
|
||||||
if isolation_level
|
if joinable?
|
||||||
connection.begin_isolated_db_transaction(isolation_level)
|
if isolation_level
|
||||||
|
connection.begin_isolated_db_transaction(isolation_level)
|
||||||
|
else
|
||||||
|
connection.begin_db_transaction
|
||||||
|
end
|
||||||
else
|
else
|
||||||
connection.begin_db_transaction
|
connection.begin_deferred_transaction(isolation_level)
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|
|
@ -65,25 +65,16 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
alias :exec_update :exec_delete
|
alias :exec_update :exec_delete
|
||||||
|
|
||||||
def begin_isolated_db_transaction(isolation) # :nodoc:
|
def begin_deferred_transaction(isolation = nil) # :nodoc:
|
||||||
raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
|
internal_begin_transaction(:deferred, isolation)
|
||||||
raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
|
end
|
||||||
|
|
||||||
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
|
def begin_isolated_db_transaction(isolation) # :nodoc:
|
||||||
ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted] = conn.get_first_value("PRAGMA read_uncommitted")
|
internal_begin_transaction(:deferred, isolation)
|
||||||
conn.read_uncommitted = true
|
|
||||||
begin_db_transaction
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def begin_db_transaction # :nodoc:
|
def begin_db_transaction # :nodoc:
|
||||||
log("begin transaction", "TRANSACTION") do
|
internal_begin_transaction(:immediate, nil)
|
||||||
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
|
|
||||||
result = conn.transaction
|
|
||||||
verified!
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def commit_db_transaction # :nodoc:
|
def commit_db_transaction # :nodoc:
|
||||||
|
@ -114,6 +105,25 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def internal_begin_transaction(mode, isolation)
|
||||||
|
if isolation
|
||||||
|
raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
|
||||||
|
raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
|
||||||
|
end
|
||||||
|
|
||||||
|
log("begin #{mode} transaction", "TRANSACTION") do
|
||||||
|
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
|
||||||
|
if isolation
|
||||||
|
ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted] = conn.get_first_value("PRAGMA read_uncommitted")
|
||||||
|
conn.read_uncommitted = true
|
||||||
|
end
|
||||||
|
result = conn.transaction(mode)
|
||||||
|
verified!
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
|
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
|
||||||
log(sql, name, async: async) do |notification_payload|
|
log(sql, name, async: async) do |notification_payload|
|
||||||
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
|
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
|
||||||
|
|
|
@ -120,7 +120,11 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
@config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
|
@config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
|
||||||
@connection_parameters = @config.merge(database: @config[:database].to_s, results_as_hash: true)
|
@connection_parameters = @config.merge(
|
||||||
|
database: @config[:database].to_s,
|
||||||
|
results_as_hash: true,
|
||||||
|
default_transaction_mode: :immediate,
|
||||||
|
)
|
||||||
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
|
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -798,7 +798,7 @@ class PessimisticLockingTest < ActiveRecord::TestCase
|
||||||
|
|
||||||
a = Thread.new do
|
a = Thread.new do
|
||||||
t0 = Time.now
|
t0 = Time.now
|
||||||
Person.transaction do
|
Person.transaction(joinable: false) do
|
||||||
yield
|
yield
|
||||||
b_wakeup.set
|
b_wakeup.set
|
||||||
a_wakeup.wait
|
a_wakeup.wait
|
||||||
|
|
|
@ -1386,6 +1386,12 @@ class TransactionTest < ActiveRecord::TestCase
|
||||||
Topic.reset_column_information
|
Topic.reset_column_information
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_sqlite_default_transaction_mode_is_immediate
|
||||||
|
assert_queries_match(/BEGIN IMMEDIATE TRANSACTION/i, include_schema: false) do
|
||||||
|
Topic.transaction { Topic.lease_connection.materialize_transactions }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_transactions_state_from_rollback
|
def test_transactions_state_from_rollback
|
||||||
|
|
Loading…
Reference in New Issue