transaction_isolation_continued 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +171 -0
- data/.gitignore +6 -0
- data/Gemfile +8 -0
- data/Gemfile.local +18 -0
- data/LICENSE +19 -0
- data/README.md +118 -0
- data/Rakefile +11 -0
- data/d +1 -0
- data/gemfiles/Gemfile.base +6 -0
- data/gemfiles/activerecord-5.2/Gemfile.base +4 -0
- data/gemfiles/activerecord-5.2/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-5.2/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-5.2/Gemfile.sqlite3 +10 -0
- data/gemfiles/activerecord-6.0/Gemfile.base +4 -0
- data/gemfiles/activerecord-6.0/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-6.0/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-6.0/Gemfile.sqlite3 +10 -0
- data/gemfiles/activerecord-6.1/Gemfile.base +4 -0
- data/gemfiles/activerecord-6.1/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-6.1/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-6.1/Gemfile.sqlite3 +10 -0
- data/gemfiles/activerecord-7.0/Gemfile.base +4 -0
- data/gemfiles/activerecord-7.0/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-7.0/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-7.0/Gemfile.sqlite3 +11 -0
- data/lib/transaction_isolation/active_record/base.rb +13 -0
- data/lib/transaction_isolation/active_record/connection_adapters/abstract_adapter.rb +32 -0
- data/lib/transaction_isolation/active_record/connection_adapters/mysql2_adapter.rb +83 -0
- data/lib/transaction_isolation/active_record/connection_adapters/postgresql_adapter.rb +81 -0
- data/lib/transaction_isolation/active_record/connection_adapters/sqlite3_adapter.rb +80 -0
- data/lib/transaction_isolation/active_record/errors.rb +10 -0
- data/lib/transaction_isolation/configuration.rb +19 -0
- data/lib/transaction_isolation/version.rb +3 -0
- data/lib/transaction_isolation.rb +51 -0
- data/test/db/all.rb +4 -0
- data/test/db/db.rb +37 -0
- data/test/db/migrations.rb +20 -0
- data/test/db/queued_job.rb +2 -0
- data/test/integration/active_record/base/isolation_level_test.rb +23 -0
- data/test/integration/active_record/connection_adapters/any_adapter/current_isolation_level_test.rb +33 -0
- data/test/integration/active_record/connection_adapters/any_adapter/current_vendor_isolation_level_test.rb +33 -0
- data/test/integration/active_record/connection_adapters/any_adapter/isolation_level_test.rb +69 -0
- data/test/integration/active_record/connection_adapters/any_adapter/supports_isolation_levels_test.rb +23 -0
- data/test/integration/active_record/connection_adapters/any_adapter/translate_exception_test.rb +46 -0
- data/test/library_setup.rb +25 -0
- data/test/log/.gitkeep +0 -0
- data/test/test_console.rb +11 -0
- data/test/test_helper.rb +12 -0
- data/test/test_runner.rb +4 -0
- data/tests +6 -0
- data/transaction_isolation_continued.gemspec +25 -0
- metadata +154 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
if defined?( ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )
|
2
|
+
|
3
|
+
module TransactionIsolation
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters # :nodoc:
|
6
|
+
module PostgreSQLAdapter
|
7
|
+
|
8
|
+
def self.included( base )
|
9
|
+
base.class_eval do
|
10
|
+
alias_method :translate_exception_without_transaction_isolation_conflict, :translate_exception
|
11
|
+
alias_method :translate_exception, :translate_exception_with_transaction_isolation_conflict
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def supports_isolation_levels?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
VENDOR_ISOLATION_LEVEL = {
|
20
|
+
:read_uncommitted => 'READ UNCOMMITTED',
|
21
|
+
:read_committed => 'READ COMMITTED',
|
22
|
+
:repeatable_read => 'REPEATABLE READ',
|
23
|
+
:serializable => 'SERIALIZABLE'
|
24
|
+
}
|
25
|
+
|
26
|
+
ANSI_ISOLATION_LEVEL = {
|
27
|
+
'READ UNCOMMITTED' => :read_uncommitted,
|
28
|
+
'READ COMMITTED' => :read_committed,
|
29
|
+
'REPEATABLE READ' => :repeatable_read,
|
30
|
+
'SERIALIZABLE' => :serializable
|
31
|
+
}
|
32
|
+
|
33
|
+
def current_isolation_level
|
34
|
+
ANSI_ISOLATION_LEVEL[current_vendor_isolation_level]
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_vendor_isolation_level
|
38
|
+
select_value( "SHOW TRANSACTION ISOLATION LEVEL" ).upcase
|
39
|
+
end
|
40
|
+
|
41
|
+
def isolation_level( level )
|
42
|
+
validate_isolation_level( level )
|
43
|
+
|
44
|
+
original_vendor_isolation_level = current_vendor_isolation_level if block_given?
|
45
|
+
|
46
|
+
execute "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL #{VENDOR_ISOLATION_LEVEL[level]}"
|
47
|
+
|
48
|
+
begin
|
49
|
+
yield
|
50
|
+
ensure
|
51
|
+
execute "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL #{original_vendor_isolation_level}"
|
52
|
+
end if block_given?
|
53
|
+
end
|
54
|
+
|
55
|
+
def translate_exception_with_transaction_isolation_conflict(*args)
|
56
|
+
exception = args.first
|
57
|
+
|
58
|
+
if isolation_conflict?(exception)
|
59
|
+
::ActiveRecord::TransactionIsolationConflict.new("Transaction isolation conflict detected: #{exception.message}")
|
60
|
+
else
|
61
|
+
translate_exception_without_transaction_isolation_conflict(*args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
ruby2_keywords :translate_exception_with_transaction_isolation_conflict if respond_to?(:ruby2_keywords, true)
|
66
|
+
|
67
|
+
def isolation_conflict?( exception )
|
68
|
+
[ "deadlock detected",
|
69
|
+
"could not serialize access" ].any? do |error_message|
|
70
|
+
exception.message =~ /#{Regexp.escape( error_message )}/i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
if defined?( ActiveRecord::ConnectionAdapters::SQLite3Adapter )
|
2
|
+
|
3
|
+
module TransactionIsolation
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters # :nodoc:
|
6
|
+
module SQLite3Adapter
|
7
|
+
|
8
|
+
def self.included( base )
|
9
|
+
base.class_eval do
|
10
|
+
alias_method :translate_exception_without_transaction_isolation_conflict, :translate_exception
|
11
|
+
alias_method :translate_exception, :translate_exception_with_transaction_isolation_conflict
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def supports_isolation_levels?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
VENDOR_ISOLATION_LEVEL = {
|
20
|
+
:read_uncommitted => 'read_uncommitted = 1',
|
21
|
+
:read_committed => 'read_uncommitted = 0',
|
22
|
+
:repeatable_read => 'read_uncommitted = 0',
|
23
|
+
:serializable => 'read_uncommitted = 0'
|
24
|
+
}
|
25
|
+
|
26
|
+
ANSI_ISOLATION_LEVEL = {
|
27
|
+
'read_uncommitted = 1' => :read_uncommitted,
|
28
|
+
'read_uncommitted = 0' => :serializable
|
29
|
+
}
|
30
|
+
|
31
|
+
def current_isolation_level
|
32
|
+
ANSI_ISOLATION_LEVEL[current_vendor_isolation_level]
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_vendor_isolation_level
|
36
|
+
"read_uncommitted = #{select_value( "PRAGMA read_uncommitted" )}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def isolation_level( level )
|
40
|
+
validate_isolation_level( level )
|
41
|
+
|
42
|
+
original_vendor_isolation_level = current_vendor_isolation_level if block_given?
|
43
|
+
|
44
|
+
execute "PRAGMA #{VENDOR_ISOLATION_LEVEL[level]}"
|
45
|
+
|
46
|
+
begin
|
47
|
+
yield
|
48
|
+
ensure
|
49
|
+
execute "PRAGMA #{original_vendor_isolation_level}"
|
50
|
+
end if block_given?
|
51
|
+
end
|
52
|
+
|
53
|
+
def translate_exception_with_transaction_isolation_conflict(*args)
|
54
|
+
exception = args.first
|
55
|
+
|
56
|
+
if isolation_conflict?(exception)
|
57
|
+
::ActiveRecord::TransactionIsolationConflict.new("Transaction isolation conflict detected: #{exception.message}")
|
58
|
+
else
|
59
|
+
translate_exception_without_transaction_isolation_conflict(*args)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
ruby2_keywords :translate_exception_with_transaction_isolation_conflict if respond_to?(:ruby2_keywords, true)
|
64
|
+
|
65
|
+
# http://www.sqlite.org/c3ref/c_abort.html
|
66
|
+
def isolation_conflict?( exception )
|
67
|
+
[ "The database file is locked",
|
68
|
+
"A table in the database is locked",
|
69
|
+
"Database lock protocol error"].any? do |error_message|
|
70
|
+
exception.message =~ /#{Regexp.escape( error_message )}/i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
ActiveRecord::ConnectionAdapters::SQLite3Adapter.send( :include, TransactionIsolation::ActiveRecord::ConnectionAdapters::SQLite3Adapter )
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'active_record/errors'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
# This exception represents both deadlocks and serialization conflicts.
|
5
|
+
# Deadlocks happen when db engine is using lock-based concurrency control.
|
6
|
+
# Serialization conflicts happen when db engine is using multi-version concurrency control.
|
7
|
+
# Often db engines combine both approaches and thus generate both types of errors.
|
8
|
+
|
9
|
+
class TransactionIsolationConflict < ::ActiveRecord::WrappedDatabaseException; end
|
10
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module TransactionIsolation
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :mysql_isolation_variable
|
4
|
+
attr_accessor :detect_mysql_isolation_variable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@mysql_isolation_variable = 'tx_isolation'
|
8
|
+
@detect_mysql_isolation_variable = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def mysql_isolation_variable=( value )
|
12
|
+
unless value.in? %w[transaction_isolation tx_isolation]
|
13
|
+
raise ArgumentError, "Invalid MySQL isolation variable '#{value}'. Supported variables include 'transaction_isolation' and 'tx_isolation'."
|
14
|
+
end
|
15
|
+
|
16
|
+
@mysql_isolation_variable = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require_relative 'transaction_isolation/version'
|
3
|
+
require_relative 'transaction_isolation/configuration'
|
4
|
+
|
5
|
+
module TransactionIsolation
|
6
|
+
|
7
|
+
# Must be called after ActiveRecord established a connection.
|
8
|
+
# Only then we know which connection adapter is actually loaded and can be enhanced.
|
9
|
+
# Please note ActiveRecord does not load unused adapters.
|
10
|
+
def self.apply_activerecord_patch
|
11
|
+
require_relative 'transaction_isolation/active_record/errors'
|
12
|
+
require_relative 'transaction_isolation/active_record/base'
|
13
|
+
require_relative 'transaction_isolation/active_record/connection_adapters/abstract_adapter'
|
14
|
+
require_relative 'transaction_isolation/active_record/connection_adapters/mysql2_adapter'
|
15
|
+
require_relative 'transaction_isolation/active_record/connection_adapters/postgresql_adapter'
|
16
|
+
require_relative 'transaction_isolation/active_record/connection_adapters/sqlite3_adapter'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.configure
|
24
|
+
config = configuration
|
25
|
+
yield(config)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.config
|
29
|
+
config = configuration
|
30
|
+
yield(config) if block_given?
|
31
|
+
config
|
32
|
+
end
|
33
|
+
|
34
|
+
if defined?( ::Rails )
|
35
|
+
# Setup applying the patch after Rails is initialized.
|
36
|
+
class Railtie < ::Rails::Railtie
|
37
|
+
config.after_initialize do
|
38
|
+
if ActiveRecord::Base.connection.adapter_name == 'Mysql2' && TransactionIsolation.config.detect_mysql_isolation_variable
|
39
|
+
mysql_version = ActiveRecord::Base.connection.select_value('SELECT version()')
|
40
|
+
if mysql_version >= '8'
|
41
|
+
TransactionIsolation.config.mysql_isolation_variable = 'transaction_isolation'
|
42
|
+
else
|
43
|
+
TransactionIsolation.config.mysql_isolation_variable = 'tx_isolation'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
TransactionIsolation.apply_activerecord_patch
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/test/db/all.rb
ADDED
data/test/db/db.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module TransactionIsolation
|
4
|
+
module Test
|
5
|
+
module Db
|
6
|
+
|
7
|
+
def self.connect_to_mysql2
|
8
|
+
::ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "mysql2",
|
10
|
+
:database => ENV['MYSQL_DB_NAME'],
|
11
|
+
:host => ENV['MYSQL_DB_HOST'],
|
12
|
+
:user => 'root',
|
13
|
+
:password => ENV['MYSQL_DB_PASS']
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.connect_to_postgresql
|
18
|
+
::ActiveRecord::Base.establish_connection(
|
19
|
+
:adapter => "postgresql",
|
20
|
+
:database => ENV['POSTGRESQL_DB_NAME'],
|
21
|
+
:host => ENV['POSTGRESQL_DB_HOST'],
|
22
|
+
:user => ENV['POSTGRESQL_DB_USER'],
|
23
|
+
:password => ENV['POSTGRESQL_DB_PASS']
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.connect_to_sqlite3
|
28
|
+
ActiveRecord::Base.establish_connection(
|
29
|
+
:adapter => "sqlite3",
|
30
|
+
:database => ":memory:",
|
31
|
+
:verbosity => "silent"
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module TransactionIsolation
|
2
|
+
module Test
|
3
|
+
module Migrations
|
4
|
+
|
5
|
+
def self.run!
|
6
|
+
c = ::ActiveRecord::Base.connection
|
7
|
+
|
8
|
+
# Queued Jobs
|
9
|
+
|
10
|
+
c.create_table "queued_jobs", :force => true do |t|
|
11
|
+
t.text "job", :null => false
|
12
|
+
t.integer "status", :default => 0, :null => false
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActiveRecordTest < Minitest::Test
|
6
|
+
|
7
|
+
class BaseTest < Minitest::Test
|
8
|
+
|
9
|
+
class IsolationLevelTest < Minitest::Test
|
10
|
+
|
11
|
+
def test_wraps_connection_isolation_level
|
12
|
+
ActiveRecord::Base.isolation_level( :serializable ) do
|
13
|
+
ActiveRecord::Base.transaction do
|
14
|
+
assert_equal( :serializable, ActiveRecord::Base.connection.current_isolation_level )
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/test/integration/active_record/connection_adapters/any_adapter/current_isolation_level_test.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActiveRecordTest < Minitest::Test
|
6
|
+
|
7
|
+
class ConnectionAdaptersTest < Minitest::Test
|
8
|
+
|
9
|
+
class AnyAdapterTest < Minitest::Test
|
10
|
+
|
11
|
+
class CurrentIsolationLevelTest < Minitest::Test
|
12
|
+
|
13
|
+
def test_returns_correct_default_isolation_level
|
14
|
+
if defined?( ActiveRecord::ConnectionAdapters::Mysql2Adapter )
|
15
|
+
assert_equal( :repeatable_read, ActiveRecord::Base.connection.current_isolation_level )
|
16
|
+
end
|
17
|
+
|
18
|
+
if defined?( ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )
|
19
|
+
assert_equal( :read_committed, ActiveRecord::Base.connection.current_isolation_level )
|
20
|
+
end
|
21
|
+
|
22
|
+
if defined?( ActiveRecord::ConnectionAdapters::SQLite3Adapter )
|
23
|
+
assert_equal( :serializable, ActiveRecord::Base.connection.current_isolation_level )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActiveRecordTest < Minitest::Test
|
6
|
+
|
7
|
+
class ConnectionAdaptersTest < Minitest::Test
|
8
|
+
|
9
|
+
class AnyAdapterTest < Minitest::Test
|
10
|
+
|
11
|
+
class CurrentVendorIsolationLevelTest < Minitest::Test
|
12
|
+
|
13
|
+
def test_returns_correct_default_vendor_isolation_level
|
14
|
+
if defined?( ActiveRecord::ConnectionAdapters::Mysql2Adapter )
|
15
|
+
assert_equal( 'REPEATABLE READ', ActiveRecord::Base.connection.current_vendor_isolation_level )
|
16
|
+
end
|
17
|
+
|
18
|
+
if defined?( ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )
|
19
|
+
assert_equal( 'READ COMMITTED', ActiveRecord::Base.connection.current_vendor_isolation_level )
|
20
|
+
end
|
21
|
+
|
22
|
+
if defined?( ActiveRecord::ConnectionAdapters::SQLite3Adapter )
|
23
|
+
assert_equal( 'read_uncommitted = 0', ActiveRecord::Base.connection.current_vendor_isolation_level )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActiveRecordTest < Minitest::Test
|
6
|
+
|
7
|
+
class ConnectionAdaptersTest < Minitest::Test
|
8
|
+
|
9
|
+
class AnyAdapterTest < Minitest::Test
|
10
|
+
|
11
|
+
class IsolationLevelTest < Minitest::Test
|
12
|
+
|
13
|
+
def test_without_a_block
|
14
|
+
original_isolation_level = ActiveRecord::Base.connection.current_isolation_level
|
15
|
+
|
16
|
+
ActiveRecord::Base.connection.isolation_level( :read_uncommitted )
|
17
|
+
assert_equal( :read_uncommitted, ActiveRecord::Base.connection.current_isolation_level )
|
18
|
+
|
19
|
+
ActiveRecord::Base.connection.isolation_level( original_isolation_level )
|
20
|
+
assert_equal( original_isolation_level, ActiveRecord::Base.connection.current_isolation_level )
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_with_a_block
|
24
|
+
original_isolation_level = ActiveRecord::Base.connection.current_isolation_level
|
25
|
+
refute_equal( :read_uncommitted, original_isolation_level )
|
26
|
+
|
27
|
+
ActiveRecord::Base.connection.isolation_level( :read_uncommitted ) do
|
28
|
+
assert_equal( :read_uncommitted, ActiveRecord::Base.connection.current_isolation_level )
|
29
|
+
ActiveRecord::Base.transaction do
|
30
|
+
assert_equal( :read_uncommitted, ActiveRecord::Base.connection.current_isolation_level )
|
31
|
+
QueuedJob.count
|
32
|
+
QueuedJob.first
|
33
|
+
assert_equal( :read_uncommitted, ActiveRecord::Base.connection.current_isolation_level )
|
34
|
+
end
|
35
|
+
assert_equal( :read_uncommitted, ActiveRecord::Base.connection.current_isolation_level )
|
36
|
+
end
|
37
|
+
|
38
|
+
assert_equal( original_isolation_level, ActiveRecord::Base.connection.current_isolation_level )
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_with_all_possible_ansi_levels
|
42
|
+
[:read_uncommitted, :read_committed, :repeatable_read, :serializable].each do |ansi_level|
|
43
|
+
|
44
|
+
QueuedJob.isolation_level( ansi_level ) do
|
45
|
+
|
46
|
+
# Some typical usage
|
47
|
+
QueuedJob.transaction do
|
48
|
+
QueuedJob.create!( :job => 'is fun' )
|
49
|
+
assert_equal( 1, QueuedJob.count )
|
50
|
+
raise ActiveRecord::Rollback
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_with_invalid_isolation_level
|
58
|
+
assert_raises( ArgumentError ) do
|
59
|
+
QueuedJob.isolation_level( :dupa )
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActiveRecordTest < Minitest::Test
|
6
|
+
|
7
|
+
class ConnectionAdaptersTest < Minitest::Test
|
8
|
+
|
9
|
+
class AnyAdapterTest < Minitest::Test
|
10
|
+
|
11
|
+
class SupportsIsolationLevelsTest < Minitest::Test
|
12
|
+
|
13
|
+
def test_returns_true
|
14
|
+
assert( ActiveRecord::Base.connection.supports_isolation_levels? )
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/test/integration/active_record/connection_adapters/any_adapter/translate_exception_test.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActiveRecordTest < Minitest::Test
|
6
|
+
|
7
|
+
class ConnectionAdaptersTest < Minitest::Test
|
8
|
+
|
9
|
+
class AnyAdapterTest < Minitest::Test
|
10
|
+
|
11
|
+
class TranslateExceptionTest < Minitest::Test
|
12
|
+
|
13
|
+
def test_does_not_break_existing_translation
|
14
|
+
assert_raises( ActiveRecord::StatementInvalid ) do
|
15
|
+
ActiveRecord::Base.connection.execute( "WE LIVE IN THE MOST EXCITING TIMES EVER" )
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_translates_low_level_exceptions_to_transaction_isolation_level
|
20
|
+
if defined?( ActiveRecord::ConnectionAdapters::Mysql2Adapter )
|
21
|
+
message = "Deadlock found when trying to get lock"
|
22
|
+
translated_exception = ActiveRecord::Base.connection.send( :translate_exception, Mysql2::Error.new( message ), message: message, sql: nil, binds: nil )
|
23
|
+
assert_equal( ActiveRecord::TransactionIsolationConflict, translated_exception.class )
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined?( ActiveRecord::ConnectionAdapters::PostgreSQLAdapter )
|
27
|
+
message = "deadlock detected"
|
28
|
+
translated_exception = ActiveRecord::Base.connection.send( :translate_exception, PG::Error.new( message ), message: message, sql: nil, binds: nil )
|
29
|
+
assert_equal( ActiveRecord::TransactionIsolationConflict, translated_exception.class )
|
30
|
+
end
|
31
|
+
|
32
|
+
if defined?( ActiveRecord::ConnectionAdapters::SQLite3Adapter )
|
33
|
+
message = "The database file is locked"
|
34
|
+
translated_exception = ActiveRecord::Base.connection.send( :translate_exception, StandardError.new( message ), message: message, sql: nil, binds: nil )
|
35
|
+
assert_equal( ActiveRecord::TransactionIsolationConflict, translated_exception.class )
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Prepares application to be tested (requires files, connects to db, resets schema and data, applies patches, etc.)
|
2
|
+
|
3
|
+
# Initialize database
|
4
|
+
require 'db/all'
|
5
|
+
|
6
|
+
case ENV['db']
|
7
|
+
when 'mysql2'
|
8
|
+
TransactionIsolation::Test::Db.connect_to_mysql2
|
9
|
+
when 'postgresql'
|
10
|
+
TransactionIsolation::Test::Db.connect_to_postgresql
|
11
|
+
when 'sqlite3'
|
12
|
+
TransactionIsolation::Test::Db.connect_to_sqlite3
|
13
|
+
else
|
14
|
+
TransactionIsolation::Test::Db.connect_to_sqlite3
|
15
|
+
end
|
16
|
+
|
17
|
+
TransactionIsolation::Test::Migrations.run!
|
18
|
+
|
19
|
+
# Load the code that will be tested
|
20
|
+
require 'transaction_isolation'
|
21
|
+
|
22
|
+
require 'logger'
|
23
|
+
ActiveRecord::Base.logger = Logger.new( File.expand_path( "#{File.dirname( __FILE__ )}/log/test.log" ) )
|
24
|
+
|
25
|
+
TransactionIsolation.apply_activerecord_patch
|
data/test/log/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Ensure that LOAD_PATH is the same as when running "rake test"; normally rake takes care of that
|
2
|
+
$LOAD_PATH << File.expand_path( ".", File.dirname( __FILE__ ) )
|
3
|
+
$LOAD_PATH << File.expand_path( "./lib", File.dirname( __FILE__ ) )
|
4
|
+
$LOAD_PATH << File.expand_path( "./test", File.dirname( __FILE__ ) )
|
5
|
+
|
6
|
+
# Boot the app
|
7
|
+
require_relative 'library_setup'
|
8
|
+
|
9
|
+
# Fire the console
|
10
|
+
require 'pry'
|
11
|
+
binding.pry
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Load test coverage tool (must be loaded before any code)
|
2
|
+
#require 'simplecov'
|
3
|
+
#SimpleCov.start do
|
4
|
+
# add_filter '/test/'
|
5
|
+
# add_filter '/config/'
|
6
|
+
#end
|
7
|
+
|
8
|
+
# Load and initialize the application to be tested
|
9
|
+
require 'library_setup'
|
10
|
+
|
11
|
+
# Load test frameworks
|
12
|
+
require 'minitest/autorun'
|
data/test/test_runner.rb
ADDED
data/tests
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "transaction_isolation/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "transaction_isolation_continued"
|
7
|
+
s.version = TransactionIsolation::VERSION
|
8
|
+
s.authors = ["Piotr 'Qertoip' Włodarek"]
|
9
|
+
s.email = ["qertoip@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/qertoip/transaction_isolation"
|
11
|
+
s.summary = %q{Set transaction isolation level in the ActiveRecord in a database agnostic way.}
|
12
|
+
s.description = %q{Set transaction isolation level in the ActiveRecord in a database agnostic way.
|
13
|
+
Works with MySQL, PostgreSQL and SQLite as long as you are using new adapters mysql2, pg or sqlite3.
|
14
|
+
Supports all ANSI SQL isolation levels: :serializable, :repeatable_read, :read_committed, :read_uncommitted.}
|
15
|
+
s.required_ruby_version = '>= 1.9.2'
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency 'rake', '~> 13.0'
|
23
|
+
s.add_development_dependency 'minitest', '5.3.4'
|
24
|
+
s.add_runtime_dependency "activerecord", ">= 5.2"
|
25
|
+
end
|