Skip to content

Commit

Permalink
Merge pull request rails#50371 from fractaledmind/ar-sqlite-immediate…
Browse files Browse the repository at this point in the history
…-transactions

Ensure SQLite transaction default to IMMEDIATE mode
  • Loading branch information
byroot authored Jul 26, 2024
2 parents ac0fa17 + 1e2c904 commit def0397
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 20 deletions.
6 changes: 6 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,14 @@ def add_transaction_record(record, ensure_finalize = true)
# Begins the transaction (and turns off auto-committing).
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
{
read_uncommitted: "READ UNCOMMITTED",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,14 @@ def full_rollback?; false; end
# = Active Record Real \Transaction
class RealTransaction < Transaction
def materialize!
if isolation_level
connection.begin_isolated_db_transaction(isolation_level)
if joinable?
if isolation_level
connection.begin_isolated_db_transaction(isolation_level)
else
connection.begin_db_transaction
end
else
connection.begin_db_transaction
connection.begin_deferred_transaction(isolation_level)
end

super
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,16 @@ def exec_delete(sql, name = "SQL", binds = []) # :nodoc:
end
alias :exec_update :exec_delete

def begin_isolated_db_transaction(isolation) # :nodoc:
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?
def begin_deferred_transaction(isolation = nil) # :nodoc:
internal_begin_transaction(:deferred, isolation)
end

with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted] = conn.get_first_value("PRAGMA read_uncommitted")
conn.read_uncommitted = true
begin_db_transaction
end
def begin_isolated_db_transaction(isolation) # :nodoc:
internal_begin_transaction(:deferred, isolation)
end

def begin_db_transaction # :nodoc:
log("begin transaction", "TRANSACTION") do
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
result = conn.transaction
verified!
result
end
end
internal_begin_transaction(:immediate, nil)
end

def commit_db_transaction # :nodoc:
Expand Down Expand Up @@ -114,6 +105,25 @@ def high_precision_current_timestamp
end

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)
log(sql, name, async: async) do |notification_payload|
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ def initialize(...)
end

@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
end

Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/locking_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ def duel(&block)

a = Thread.new do
t0 = Time.now
Person.transaction do
Person.transaction(joinable: false) do
yield
b_wakeup.set
a_wakeup.wait
Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/cases/transactions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,12 @@ def test_sqlite_add_column_in_transaction
Topic.reset_column_information
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

def test_transactions_state_from_rollback
Expand Down

0 comments on commit def0397

Please sign in to comment.